brnfck.py
A project where i made a tiny interpreter for a tiny language, with a tiny bit of error on the
part of my past self.
The first project i want to log here is my brainfuch interpreter i code-golfed in python. For those of you who aren't familiar, brainfuck is an esolang, or esoteric language. It was developed to be turing complete, essentially emulating a literal turing machine, while being simple to implement an interpretr for. for more info you can go to https://esolangs.org/wiki/Brainfuck.
I'll start with where i started before: a basic interpreter written in python that is theoretically readable.
our first few variables here are pretty simple. first we have the
code itself. this is the string of brainfuck source code we wish to interpret,
taken as
user input.
next we have the tape, representing the turing machine's infinite tape of integers.
we
initialize it with zero to match the initialization rules of the language.
finally, we have ind, which acts a a sort of stand in for the tape header or a
pointer
to the current element.
here, we get to the meat of things.
we are looping over the elements in code, which in this case are single character
commands. if you have been a python dev for a while you might have heard it said that using
for i in range(len(iterable): is the sign of a new python developer, but
here, i
had a reason.
ignoring that for now, we look at the actual decoding of the code. we have eight if statements here (python hadn't added match yet when i wrote this), one for each command. any symbol that did not match one of these was simply ignored, the standard way to "implement" comments in brainfuck.
our first two operate in a very simple way. if the symbol is + or -,
then
we increment or decrement the value of the cell at the index by one.
now we have the movement symbols. to similate this being an infinite tape, i made the terrible
decision to always add a cell to whichever end you are trying to move. on right ward motion, we
add
a cell to the list and increase the index by one to move it over. for leftward we get a nice
little
convenience in that by addign a cell at the "front" of the array, we increase the index of every
existing cell. so we can keep our ind value since ind-1 before becomes ind-1+1
after.
next we get to input/output. we are making an assumption here that the user will use the system correctly. that is, if on input you enter more than one character or a non-ascii character, the program crashes immediately. i consider this a feature, but mostly didn't want morecode to have to shrink. ther than that we are doing a pretty basic ascii-to-int conversion on the way in and int-to-ascii on the way out.
this is a spot where a decision i made has an effect on the running of this interpreter. many versions of brainfuck restrict the value space to 256 byte integers, where as i allow as big an integer as your system will allow. any brainfuck code written with the expectation of overflowing will fail. an example of this the fourth Hello, World! example on the esolang wiki entry:
finally we get to what i think are the most interesting commands, implementation-wise. the loop
controls: [ and ].
loosely how these work is they must come in pairs. upon reading an open bracket, we check the
value
of the cell at the head, and if it is zero, we skip to the instruction after the closing
bracket.
similarly, if we read a closing bracket, we skip BACK to the paired opening bracket. this simple
description gives rise to the need to count brackets, so that we can pair them up appropriately.
if
i were writing a smarter system, i'd probably precalculate these positions, but it was fun to
have
each command be fully self-contained.
so now, how does the bracket counting work? well, its pretty simple actually. when we first find an open bracket and need to jump to it's closing tag (cell has value 0) we begin a loop that reads the code. this bit is the payoff from above about the
for i in range(len(iterable):
business. since we need to be able to track forward and back with this on the instruction list, it
became necessary to have access to the index itself.
we begin by setting a depth of zero, and an index offset (t) of zero. we then move
right along the instructions until we find a bracket. if it's an open bracket, we increase the
depth, and if it's a closing bracket we decrease the depth...
It was at this point in the write-up that i realized my old code (from 2023) was just plain wrong. that said, i felt the need to fix it so here is the fixed version:
in a lesson in humility, i was in fact wrong to use the for loop. additionally, the third if
within
the [ handling had both portions of the and inverted.
in defiance of humility, there was NOT something wrong with the shift operations, past me is just a liar.
ahem, now then, we start at depth zero, and each opening bracket increases the depth, while each closing bracket decreases it. when the depth has reached zero, we exit the loop, allowing the program counter to increment to get us to the next instruction on the tape. our handling of the closing bracket works essentially the same way, adding only an extra step back to neutralize the program counter's standard incrementation.
At last, the good part! behold here, my minified interpreter, brnfck.py:
pretty nice, isn't it?
...
*crickets*
...
fine, we can talk about how it works.
the first thing i did, as is so often the case when code golfing is to replace variable names with single characters. so,
- code -> c
- tape -> t
- instruction_pointer -> n
- ind -> i
- depth -> d
next i realized we didn't need to re-check the current code character in the loop control handling, so we dropped that.
then i remember, from taking classes in python 2, that you could use semi-colons to separate statements in python so several sections got condensed to one line. this would turn out to be necessary for the biggest savings
i felt like a fucking genius figuring out that instead of the ifs (remembering that match didn't
exist yet), i could stuff the whole logic into a dictionary and use the literal code passed in
as
the keys and pass the code as a string to the exec function. this had the lovely
effect
of making comments a braeking feature now, but let's be honest here, if you wanted to run
brainfuck
you are NOT using this guy.
the sub-loops for the loop handling did unfortunately require multiline strings with whitespace to be functional, but i think overall it turned out ok!
second project!
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta.
Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. Sed dignissim lacinia nunc.