/* Skullfuck -- A dead simple, non-optimizing Brainfuck compiler. Copyright (C) 2017 Jakob Kreuze, All Rights Reserved. Skullfuck is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Skullfuck is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Skullfuck. If not, see . */ #include #include #include #include #include #include #include #if defined(x86_64) #define ARCH "x86_64" #define OS "Linux" #define INC_DATA_PTR "\tincq %rsi\n" #define ADD_DATA_PTR "\taddq $0x%x, %%rsi\n" #define DEC_DATA_PTR "\tdecq %rsi\n" #define SUB_DATA_PTR "\tsubq $0x%x, %%rsi\n" #define INC_DATA "\tincb (%rsi)\n" #define ADD_DATA "\taddb $0x%x, (%%rsi)\n" #define DEC_DATA "\tdecb (%rsi)\n" #define SUB_DATA "\tsubb $0x%x, (%%rsi)\n" #define PRINT_DATA "\tmovq $0x01, %rax\n" \ "\tmovq $0x01, %rdi\n" \ "\tsyscall\n" #define READ_DATA "\tmovq $0x00, %rax\n" \ "\tmovq $0x00, %rdi\n" \ "\tsyscall\n" #define DATA_IS_ZERO "\tmovb (%rsi), %cl\n" \ "\ttestb %cl, %cl\n" #define JMP_END "\tjz E%d\n" #define JMP_START "\tjnz S%d\n" #define PRELUDE "\t.section .bss\n" \ "\t.comm mem, %d\n" \ "\t.section .text\n" \ "\t.globl _start\n" \ "_start:\n" \ "\tmovq $mem, %%rsi\n" \ "\tmovq $0x01, %%rdx\n" #define EXIT "\tmovq $0x3c, %rax\n" \ "\tmovq $0x00, %rdi\n" \ "\tsyscall\n" #elif defined(i686) #define ARCH "i686" #define OS "Linux" #define INC_DATA_PTR "\tincl %ecx\n" #define ADD_DATA_PTR "\taddl $0x%x, %%ecx\n" #define DEC_DATA_PTR "\tdecl %ecx\n" #define SUB_DATA_PTR "\tsubl $0x%x, %%ecx\n" #define INC_DATA "\tincb (%ecx)\n" #define ADD_DATA "\taddb $0x%x, (%%ecx)\n" #define DEC_DATA "\tdecb (%ecx)\n" #define SUB_DATA "\tsubb $0x%x, (%%ecx)\n" #define PRINT_DATA "\tmovl $0x04, %eax\n" \ "\tmovl $0x01, %ebx\n" \ "\tint $0x80\n" #define READ_DATA "\tmovl $0x03, %eax\n" \ "\tmovl $0x00, %ebx\n" \ "\tint $0x80\n" #define DATA_IS_ZERO "\tmovb (%ecx), %al\n" \ "\ttestb %al, %al\n" #define JMP_END "\tjz E%d\n" #define JMP_START "\tjnz S%d\n" #define PRELUDE "\t.section .bss\n" \ "\t.comm mem, %d\n" \ "\t.section .text\n" \ "\t.globl _start\n" \ "_start:\n" \ "\tmovl $mem, %%ecx\n" \ "\tmovl $0x01, %%edx\n" #define EXIT "\tmovl $0x01, %eax\n" \ "\tmovl $0x00, %ebx\n" \ "\tint $0x80\n" #endif #define MAX_CELLS 30000 #define MAX_LOOP_DEPTH 64 #define SKULLFUCK_VERSION "0.2.0" #define EXIT_SUCCESS 0 #define EXIT_FAILURE 1 /* Structure for storing runtime parameters. */ struct params { bool compile_only; char *in, *out, *as; }; /* A stack data structure for storing loop depth. */ struct loop_stack { int _mem[MAX_LOOP_DEPTH]; int _stack_index; }; /* Pushes `n` onto the stack specified by `s`. */ static void push_loop_index(struct loop_stack *s, int n) { s->_mem[s->_stack_index] = n; s->_stack_index++; } /* Pops the top of `s` off and returns it. */ static int pop_loop_index(struct loop_stack *s) { s->_stack_index--; return s->_mem[s->_stack_index]; } /* Minor optimization subroutine. Increments the `in` pointer to beyond the collection of adjacent `op` characters, and returns the number of operations counted. */ static int reduce(char **in, char op) { int i; for (i = 0; (*in)[i] == op; i++); *in += i - 1; return i; } /* Converts the brainfuck instructions read from `in` to assembly instructions and writes them to `out`. `in` is expected to be null-terminated. */ static void write_instructions(FILE *out, char *in) { int next_loop = 0, cur_loop = 0, op_count; struct loop_stack *loops = calloc(sizeof(struct loop_stack), 1); for (char *cur = in; *cur != '\0'; cur++) { switch(*cur) { case '>': op_count = reduce(&cur, '>'); if (op_count > 1) { fprintf(out, ADD_DATA_PTR, op_count); } else { fputs(INC_DATA_PTR, out); } break; case '<': op_count = reduce(&cur, '<'); if (op_count > 1) { fprintf(out, SUB_DATA_PTR, op_count); } else { fputs(DEC_DATA_PTR, out); } break; case '+': op_count = reduce(&cur, '+'); if (op_count > 1) { fprintf(out, ADD_DATA, op_count); } else { fputs(INC_DATA, out); } break; case '-': op_count = reduce(&cur, '-'); if (op_count > 1) { fprintf(out, SUB_DATA, op_count); } else { fputs(DEC_DATA, out); } break; case '.': fputs(PRINT_DATA, out); break; case ',': fputs(READ_DATA, out); break; case '[': cur_loop = next_loop; push_loop_index(loops, next_loop++); fprintf(out, "S%d:\n", cur_loop); fputs(DATA_IS_ZERO, out); fprintf(out, JMP_END, cur_loop); break; case ']': cur_loop = pop_loop_index(loops); fprintf(out, "E%d:\n", cur_loop); fputs(DATA_IS_ZERO, out); fprintf(out, JMP_START, cur_loop); break; } } fputs(EXIT, out); free(loops); } /* Prints error information and terminates with minimal cleanup. */ static void panic(char *msg) { perror(msg); fprintf(stderr, "Fatal error encountered. Terminating!\n"); exit(EXIT_FAILURE); } /* Attempts to create a new file at `path`, and writes a boilerplate prelude to initialize memory segments and load some registers with constant values. A FILE pointer will be returned, or NULL if the file could not be created. */ static FILE *out_init(char *path) { FILE *fp = fopen(path, "w+"); if (fp == NULL) return fp; fprintf(fp, PRELUDE, MAX_CELLS); return fp; } /* Returns a buffer containing the contents of `path`. */ static char *read_infile(char *path) { FILE *fp = fopen(path, "r"); if (fp == NULL) panic(path); fseek(fp, 0, SEEK_END); size_t len = ftell(fp); char *in = malloc(len); fseek(fp, 0, SEEK_SET); fread(in, len, 1, fp); fclose(fp); return in; } /* Assembles and links the compiled source code into a binary. */ static void create_binary(struct params p) { pid_t pid; char *const as_argv[] = {"as", "-o", "/tmp/skullfuck_tmp.o", p.as, NULL}; char *const ld_argv[] = {"ld", "-o", p.out, "/tmp/skullfuck_tmp.o", NULL}; if ((pid = fork()) == -1) { panic("fork"); } else if (pid == 0) { execv(AS, as_argv); } else { wait(NULL); if ((pid = fork()) == -1) { panic("fork"); } else if (pid == 0) { execv(LD, ld_argv); } else { wait(NULL); } } if (!p.compile_only) remove("/tmp/skullfuck_tmp.s"); remove("/tmp/skullfuck_tmp.o"); } /* Parses command-line arguments into a params structure. */ static struct params parse_args(int argc, char **argv) { struct params p = {0}; if (argc < 2) { fprintf(stderr, "USAGE: %s [options] (infile)\n", argv[0]); exit(EXIT_FAILURE); } int cur = 0, opt_index = 0, count = 0; static struct option long_opts[] = { {"help", no_argument, NULL, 'h'}, {"version", no_argument, NULL, 'v'}, {NULL, 0, NULL, 0} }; do { count++; cur = getopt_long(argc, argv, "hvSo:", long_opts, &opt_index); switch (cur) { case 'h': printf("A dead simple, non-optimizing Brainfuck compiler.\n\n" "\t-h, --help\tDisplay this help page and exit.\n" "\t-v, --version\tDisplay versioning information and " "exit\n\n" "\t-S\t\tStop after the stage of compilation proper; " "do not assemble.\n" "\t-o file\t\tPlace the output in file.\n"); exit(EXIT_SUCCESS); case 'v': printf("Skullfuck compiler for Brainfuck, version %s.\n" "Toolchain for %s %s.\nDeveloped by Jakob. " "\n", SKULLFUCK_VERSION, ARCH, OS); exit(EXIT_SUCCESS); case 'S': p.compile_only = true; break; case 'o': count++; p.out = optarg; break; } } while (cur >= 0); if (count >= argc) { fprintf(stderr, "Fatal error: No input files.\n"); exit(EXIT_FAILURE); } p.in = argv[count]; p.as = "/tmp/skullfuck_tmp.s"; if (p.compile_only) { p.out = p.out == NULL ? "./out.s" : p.out; p.as = p.out; } else if (p.out == NULL) { p.out = "./a.out"; } return p; } int main(int argc, char **argv) { struct params p = parse_args(argc, argv); FILE *fp = out_init(p.as); if (fp == NULL) panic(p.as); char *in = read_infile(p.in); write_instructions(fp, in); fclose(fp); if (!p.compile_only) create_binary(p); free(in); return EXIT_SUCCESS; }