I haven't been able to learn brainfuck before, until jailctf 2025 insisted I write some brainfuck code that is also valid Python! Surely, as a Python enthusiast, I'd figure out how the hell to do this...
Edited by me to help with debugging, JailCTF feel free to implement this incredible improvement!!!
#!/usr/local/bin/python3
from bfi.bfi import interpret
def bf_eval(code: str) -> str:
return interpret(code, input_data='', buffer_output=True)
def py_eval(code: str) -> str:
return str(eval(code))
code = input('> ')
if any(c not in '<>-+.,[]' for c in code):
print('bf only pls')
exit()
lhs = bf_eval(code)
rhs = py_eval(code)
print('bf:',lhs)
print('py:',rhs)
if lhs == rhs:
print(open('flag.txt', 'r').read())Well we need both of them to output the same thing. What's a bit funny is that Python has implicit output from eval whereas brainfuck has explicit output via commands.
Reading through the source code of this brainfuck interpreter, it is fairly standard, it can only output when a . literal is present: https://github.com/eriknyquist/bfi/blob/master/bfi/__init__.py (by the way, what in the actual fuck, why is the code in the __init__ file, that is the most Brainfuckian and least Pythonic way to do it). Here we also notice , does nothing when there is no input, very nice, helps with writing valid Python.
But there is literally no way in hell to put in just 1 . in the Python source code with this charset, you'd need ..., because . is getattr and there are no attrs to get. So I try and find what return values have 3 repeated characters (since note in brainfuck you can't jump over code directly, you only have while loops, so a basic block is kind of defined by the source code itself, so if there is ... at any point in the code either all 3 will run or 0 will run)
Unfortunately all we have is "Ellipsis" which definitely isn't the same thing as "EEEllliiipppsssiiisss" and stuff like "[], [], []" which looks a bit more promising but is still asymmetric for many reasons. Hmmm
Wait we have > which means we have True so we can maybe make numbers. However there is a subtle issue in that if we do true + true like [[]]>[]+[[]]>[] order of operations bites us as > is called last. But there is a super evil trick which is to use [] as a grouping block like [val][True] which will be treated as a unit by literally every other operation in Python. So here's true and false:
true = '[[[]]>[]][[]>[]]'
false = '[[[]]<[]][[]>[]]'Anyway this makes it super easy to construct a repeating number, say 111, which is perfectly ok as true+true+true+... definitely not an absurd amount of code and the python parser will definitely be ok with this. Then we try to run it in Brainfuck and it... freezes?
The + gets called in true + true and the issue with it is that it adds 1 to the current position on the tape which means that the next while loop will run forever because there isn't a + inside the definition of true itself. Hmmmmmm
Note this brainfuck wraps around at 256 down to 0 so we can add 256 times to cancel. We can't add any + on the inside of a square bracket though because the only meaningful thing we can do is []+[] gives [] but notice we're using a bracket to fix the outer bracket, but guess what the inner bracket is /also/ a while loop so it's turtles all the way down. However we can do another trick which is that Python is a well designed language and unary + can stack (I'm looking at you, C) so you can do +++++++True and it'll give you 1 just the same. So you need to use identity_add which is literally 256 pluses just to make brainfuck do nothing successfully.
identity_add = '+'*256Now we need to push literal "1" onto the tape and then print it with ... in brainfuck land. There is another issue we need to deal with now, which is that we need to enter the loop and execute it exactly one time so that we do the 3 prints just once. This is not too bad, you can do -[+ LOOP] which will subtract 1 then go into the loop then add 1 back in so that it cancels out. However notice this screws up the value of the bracket in Python land. Thus we commit more sorcery and cancel the minus with a minus and do --[++ LOOP] instead which makes sure that the addition still remains valid.
one = ord('1')*'+'
neg_one = ord('1')*'-' # brings tape back to 0 to exit loop
++[--{one}...{neg_one}]So once you represent literal "1" as a lot of pluses you run into another hurdle in that ++++++++ is not valid Python as Python is not a functional language where you can push keyword-functions onto the stack whenever you want like Julia. You also need a way to somehow have the entire block return a safe value back in Python land, not Ellipsis or whatever. One way to do this is to call each basic block into the False constant we constructed earlier so that it returns 0 overall in Python land, but we need to be careful to only use False once the tape is 0 again since otherwise it'll loop forever again, with {one}[...,{neg_one}{false}].
So here is the block we get:
block = f'++[--{one}[...,{neg_one}{false}][{true}]][{false}]'Basically all this does is it computes the value 1 in Brainfuck land, but that leaves [..., false] on the stack, which we then get the false out, which we then turn into an rvalue with the [0][False] trick (technically it is an rvalue already, we turn it into an extra rvalue, call it super-rvalue, where the + actually binds to it rather than getting intercepted by the > in the true/false definition)
So now we take the two basic blocks that generate 111 in Python but do nothing in brainfuck and generate 111 in brainfuck but generate +0 in Python and we get this monstrosity:
true = '[[[]]>[]][[]>[]]'
false = '[[[]]<[]][[]>[]]'
identity_add = '+'*256
oneoneone_py = identity_add.join(true for i in range(111))
one = ord('1')*'+'
neg_one = ord('1')*'-'
block = f'++[--{one}[...,{neg_one}{false}][{true}]][{false}]'
print(f'++[--{oneoneone_py},{block}][{false}]')Idk, it looks pretty readable to me, kinda cool how simple it ended up. Now I know 1% more brainfuck!!!