I have made a very simple Brainfuck to NASM converter, that is usable for practically all programs. It has one trivial optimisation (to subsitute ADD
for INC
with large numbers). How can I make the generated code smaller and/or faster? I would prefer smaller over faster, if I had to choose.
Here is the code:
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <limits.h>
#include <unistd.h>
#include <sys/types.h>
static void usage(const char *);
static void compile_and_write(int);
static void print_header(void);
static void print_footer(void);
static FILE *in = NULL;
static FILE *out = NULL;
int
main(
int argc,
char *argv[])
{
int ch;
int opt;
while ((opt = getopt(argc, argv, "i:o:h")) != -1) {
switch (opt) {
case 'i':
in = fopen(optarg, "r");
break;
case 'o':
out = fopen(optarg, "w");
break;
case 'h':
usage(argv[0]);
return EXIT_SUCCESS;
default:
usage(argv[0]);
return EXIT_FAILURE;
}
}
if (in == NULL) {
in = stdin;
}
if (out == NULL) {
out = stdout;
}
print_header();
while ((ch = fgetc(in)) != EOF) {
compile_and_write(ch);
}
print_footer();
fclose(in);
fclose(out);
return EXIT_SUCCESS;
}
static void
print_header(
void)
{
fputs("[bits 64]\n", out);
fputs("[section .bss]\n", out);
fputs("mem:resb 32768\n", out);
fputs("[section .text]\n", out);
fputs("[global _start]\n", out);
fputs("putc:\n", out);
fputs("xor rax, rax\n", out);
fputs("inc rax\n", out);
fputs("xor rdi, rdi\n", out);
fputs("inc rdi\n", out);
fputs("xor rdx, rdx\n", out);
fputs("inc rdx\n", out);
fputs("syscall\n", out);
fputs("ret\n", out);
fputs("getc:\n", out);
fputs("xor rax, rax\n", out);
fputs("xor rdi, rdi\n", out);
fputs("xor rdx, rdx\n", out);
fputs("inc rdx\n", out);
fputs("syscall\n", out);
fputs("ret\n", out);
fputs("_start:\n", out);
fputs("mov rsi, mem\n", out);
}
static void
print_footer(
void)
{
fputs("mov rax, 60\n", out);
fputs("xor rdi, rdi\n", out);
fputs("syscall\n", out);
}
static void
compile_and_write(
int ch)
{
static int loop = 0;
static int loops[0x10000] = {0};
static size_t lp = 0;
static unsigned cnt = 1;
static int last = ' ';
if (ch == last) {
++cnt;
return;
}
switch (last) {
case '+':
if (cnt == 1) {
fputs("inc byte [rsi]\n", out);
} else {
fprintf(out, "add byte [rsi], %u\n", cnt);
}
break;
case '-':
if (cnt == 1) {
fputs("dec byte [rsi]\n", out);
} else {
fprintf(out, "sub byte [rsi], %u\n", cnt);
}
break;
case '>':
if (cnt == 1) {
fputs("inc rsi\n", out);
} else {
fprintf(out, "add rsi, %u\n", cnt);
}
break;
case '<':
if (cnt == 1) {
fputs("dec rsi\n", out);
} else {
fprintf(out, "sub rsi, %u\n", cnt);
}
break;
case '.':
while (cnt--) {
fputs("call putc\n", out);
}
break;
case ',':
while (cnt--) {
fputs("call getc\n", out);
}
break;
case '[':
while (cnt--) {
loops[lp] = loop++;
fprintf(out, "L%d:\n", loops[lp]);
fprintf(out, "cmp byte [rsi], 0\n");
fprintf(out, "jz E%d\n", loops[lp]);
++lp;
}
break;
case ']':
while (cnt--) {
--lp;
fprintf(out, "jmp L%d\n", loops[lp]);
fprintf(out, "E%d:\n", loops[lp]);
}
break;
default:
break;
}
last = ch;
cnt = 1;
}
static void
usage(
const char *pname)
{
fprintf(stderr, "usage: %s [-h] -i input -o output\n", pname);
}
Compilation command:
$ clang -Weverything -Werror -O2 -march=native -s -o bfc bfc.c
Usage (assume brainfuck input is mandelbrot.b
and output is mandelbrot
):
$ ./bfc -i mandelbrot.b -o mandelbrot.asm
$ nasm -Ox -w+all -w+error mandelbrot.asm
$ ld.gold -s -o mandelbrot mandelbrot.o
Note: this generates x86_64 assembly code for Linux (or compatible kernels) only. I might add i386 support later (perhaps in follow-up post).
compile_and_write(0);
after the while loop; \$\endgroup\$