Dear Mods of dev.to, please bear with me for this one. This post will have a few profanities, as you might guess from the title. However, these profanities are names of packages and actual technical terms. None of these is meant to offend anyone. I’ll try to censor most of them where sensible.
And here we go again. Stuff no one should probably do in production, because it’s either completely and utterly useless, will confuse the living heck out of everyone having to maintain it, or make everyone scream “No” and “Please don’t”.
Esoteric programming languages it is this time. Specifically, Brainf**k, a dialect of Ook! that sadly can't be understood by Orang Utans. And no, I refuse to believe that it's actually the other way around.
Brainf**k is notoriously hard to read. Mostly because its commands are single characters that move the stack pointer by 1, increase or decrease the value it points at by 1, and loop as long as the current stack element the pointer is pointing to is greater than 0.
I've recently written a post about using and creating unplugins where I mentioned adding Brainf**k support and thought to myself:
So here we are. If you ever wanted to build half your app in a cumbersome, convoluted, unreadable way, the time is now! And you can even open-source it and let others do the same, too!
Creating the Unplugin boilerplate
Straightforward. We also need another dependency called "hirnf**k", a library that transpiles out Brainf*k code to JS. For the non-German-speaking folks, the name of the package translates to "brainf*k", so there's that.
So, let's open a terminal and get going:
$ px degit unplugin/unplugin-starter unplugin-bf
bash: px: command not found
Whoops. Typo. Yes, this already started badly.
$ npx degit unplugin/unplugin-starter unplugin-bf
$ cd unplugin-bf/ && npm i -s hirnfick
Much better.
This creates an unplugin and installs our source-to-source compiler. We'll implement the compilation in the unplugin's src/index.ts file.
For that, let's first look at hirnf**k's documentation. It includes an ESM example we can slightly modify. Here's what the example looks like:
import hirnfick from "https://jspm.dev/hirnfick";
const helloWorldBF = '++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.'
+ '+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.';
try {
const helloJs = hirnfick.compileToJsWeb(helloWorldBF);
const runHello = new Function(`${helloJs} return main().output.trim();`);
console.log(runHello());
} catch (err) {
console.error(`Error: ${err.message}`);
}
Perfect, right? It creates an executable function and even contains a Hello World in Brainf**k already!
Implementing everything
It's surprisingly straightforward. Luckily, the unplugin starter already gives us most of the boilerplate, we only need to fill in the gaps. Let's look at what the starter brought:
import type { UnpluginFactory } from 'unplugin'
import type { Options } from './types'
import { createUnplugin } from 'unplugin'
export const unpluginFactory: UnpluginFactory<Options | undefined> = options => ({
name: 'unplugin-starter',
transformInclude(id) {
return id.endsWith('main.ts')
},
transform(code) {
return code.replace('__UNPLUGIN__', `Hello Unplugin! ${options}`)
},
})
export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)
export default unplugin
First, we change the transformInclude function. We want it to check for .bf files instead of the main.ts. While we're at it, we also rename the unplugin and empty the transform function. The options are also not necessary, so let's remove them, too:
// ...
export const unpluginFactory: UnpluginFactory<Options | undefined> = () => ({
name: 'unplugin-bf',
transformInclude(id) {
return id.endsWith('.bf')
},
transform(code) {
return code
},
})
We're actually almost there already. Next, we actually copy/paste hirnf**k's example unto the transform function and alter it a bit:
import type { UnpluginFactory } from 'unplugin'
import type { Options } from './types'
import hirnfick from 'hirnfick'
import { createUnplugin } from 'unplugin'
export const unpluginFactory: UnpluginFactory<Options | undefined> = () => ({
name: 'unplugin-bf',
transformInclude(id) {
return id.endsWith('.bf')
},
transform(code) {
const transformed = hirnfick.compileToJsWeb(code)
const wrapped = `export default new Function(\`${transformed} return main().output.trim();\`)`
return wrapped
},
})
export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)
export default unplugin
And believe it or not, we're done. We now have Brain**k support in our JS apps via our lovely unplugin.
How it works
Now, how does this work? First, it reads the file's content and passes it as a string (the code parameter) to the transform function. We then tell hirnf*k to compile it to JS. That's, again, a string. That string is then wrapped into another string that contains the standard export default stuff with an inline function. That function returns whatever the compiled Brainf*k code would try to print via the . command, and trims all the whitespace from it.
Let's simplify the Brainf**k code to this bit:
+-.
The compiled JS would look like this:
export default new Function(`let position = 0;
const cells = [0];
let output = '';
function putchar() {
output += String.fromCharCode(cells[position]);
}
function main() {
if (cells[position] < 255) {
cells[position] += 1;
}
if (cells[position] > 0) {
cells[position] -= 1;
}
putchar(String.fromCharCode(cells[position]));
return { cells, output };
}
return main().output.trim();`)
We can clearly see where the Brainf**k code lives:
-
+becomescells[position] += 1; -
-becomescells[position] -= 1; -
.becomesputchar(String.fromCharCode(cells[position]));
Trying it out
Let's try it out! Luckily, the unplugin starter contains a playground that already sets everything up for us.
We create a new file called hello-world.bf and add the Hello World there:
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
(I tried syntax highlighting, but... well... you know...)
Next, in the example's main.ts, we import the file and execute the function it returns:
import HelloWorld from './hello-world.bf'
document.body.innerHTML = HelloWorld()
VSCode doesn’t like this in the slightest. Perhaps it knows that we want to run Brainf**k. What it doesn’t know is that .bf files return JS code, it doesn’t know that the unplugin allows that, and it will complain. We don’t care, though. Perhaps someone will eventually write a VSCode plugin to go with our unplugin. You never know.
We then go to the playground, install dependencies and run this thing_
$ cd playground
$ npm i && npm run dev
That'll start the dev server on localhost, which we open in the browser. And lo and behold:
It actually works!
However, please, please do not use Brainf**k in your production app. But if you're brave enough, you may add a few extra things yourself:
- Add input support via
, - Add sourcemaps (good luck)
- Support Ook!
Why does no one think of the Orang Utans.
I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❤️! I write tech articles in my free time and like to drink coffee every once in a while.
If you want to support my efforts, you can offer me a coffee ☕! You can also support me directly via Paypal! Or follow me on Bluesky 🦋!



Top comments (0)