Initial commit
This commit is contained in:
commit
128fb157b3
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
12
Makefile
Normal file
12
Makefile
Normal file
@ -0,0 +1,12 @@
|
||||
# All we need is C99 and POSIX, which this should make available
|
||||
CFLAGS = -std=gnu99
|
||||
NAMES = bfc-amd64-linux
|
||||
|
||||
all: $(NAMES)
|
||||
|
||||
%: %.c
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@
|
||||
clean:
|
||||
rm -f $(NAMES)
|
||||
|
||||
.PHONY: all clean
|
49
README.adoc
Normal file
49
README.adoc
Normal file
@ -0,0 +1,49 @@
|
||||
bfc
|
||||
===
|
||||
|
||||
'bfc' is a small, fast, self-contained, optimizing Brainfuck compiler for Linux
|
||||
on Intel x86-64.
|
||||
|
||||
Also included are several interpreters in various states of sophistication that
|
||||
document my progress as I was writing this, from the simplest approach to an
|
||||
optimizing JIT compiler.
|
||||
|
||||
It's pretty easy to retarget the compiler, it just means redoing half the work.
|
||||
The compiler itself is platform agnostic.
|
||||
|
||||
Building
|
||||
--------
|
||||
Build dependencies: a C99 compiler +
|
||||
Runtime dependencies: Linux
|
||||
|
||||
$ git clone https://github.com/pjanouch/bfc.git
|
||||
$ cd bfc
|
||||
$ make
|
||||
|
||||
To obtain dumps of the intermediate representation, compile with `-DDEBUG`:
|
||||
|
||||
$ make CPPFLAGS=-DDEBUG
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
./bfc-amd64-linux [INPUT-FILE] [OUTPUT-FILE]
|
||||
|
||||
When no input file is specified, stdin is used. Similarly, the default output
|
||||
filename is a.out. The resulting file can be run on the target platform.
|
||||
|
||||
Contributing and Support
|
||||
------------------------
|
||||
Use this project's GitHub to report any bugs, request features, or submit pull
|
||||
requests. If you want to discuss this project, or maybe just hang out with
|
||||
the developer, feel free to join me at irc://irc.janouch.name, channel #dev.
|
||||
|
||||
License
|
||||
-------
|
||||
'bfc' is written by Přemysl Janouch <p.janouch@gmail.com>.
|
||||
|
||||
You may use the software under the terms of the ISC license, the text of which
|
||||
is included within the package, or, at your option, you may relicense the work
|
||||
under the MIT or the Modified BSD License, as listed at the following site:
|
||||
|
||||
http://www.gnu.org/licenses/license-list.html
|
723
bfc-amd64-linux.c
Normal file
723
bfc-amd64-linux.c
Normal file
@ -0,0 +1,723 @@
|
||||
// This is an exercise in futility more than anything else
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xcalloc (size_t m, size_t n)
|
||||
{
|
||||
void *p = calloc (m, n);
|
||||
if (!p)
|
||||
exit_fatal ("calloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->len = 0;
|
||||
self->str = xcalloc (1, (self->alloc = 16));
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Application -------------------------------------------------------------
|
||||
|
||||
enum command
|
||||
{
|
||||
RIGHT, LEFT, INC, DEC, IN, OUT, BEGIN, END,
|
||||
SET, EAT, INCACC, DECACC
|
||||
};
|
||||
|
||||
bool grouped[] = { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 };
|
||||
struct instruction { enum command cmd; int offset; size_t arg; };
|
||||
#define INSTRUCTION(c, o, a) (struct instruction) { (c), (o), (a) }
|
||||
|
||||
// - - Debugging - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
#ifdef DEBUG
|
||||
static void
|
||||
debug_dump_instruction (FILE *fp, const struct instruction *in)
|
||||
{
|
||||
const char *name;
|
||||
switch (in->cmd)
|
||||
{
|
||||
case RIGHT: name = "RIGHT "; break;
|
||||
case LEFT: name = "LEFT "; break;
|
||||
case INC: name = "INC "; break;
|
||||
case DEC: name = "DEC "; break;
|
||||
case OUT: name = "OUT "; break;
|
||||
case IN: name = "IN "; break;
|
||||
case BEGIN: name = "BEGIN "; break;
|
||||
case END: name = "END "; break;
|
||||
case SET: name = "SET "; break;
|
||||
case EAT: name = "EAT "; break;
|
||||
case INCACC: name = "INCACC"; break;
|
||||
case DECACC: name = "DECACC"; break;
|
||||
}
|
||||
fprintf (fp, "%s %zu", name, in->arg);
|
||||
if (in->offset != 0)
|
||||
fprintf (fp, " [%d]", in->offset);
|
||||
fprintf (fp, "\n");
|
||||
}
|
||||
|
||||
static void
|
||||
debug_dump (const char *filename, struct instruction *in, size_t len)
|
||||
{
|
||||
FILE *fp = fopen (filename, "w");
|
||||
long indent = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (in[i].cmd == END)
|
||||
indent--;
|
||||
for (long k = 0; k < indent; k++)
|
||||
fputs (" ", fp);
|
||||
debug_dump_instruction (fp, &in[i]);
|
||||
if (in[i].cmd == BEGIN)
|
||||
indent++;
|
||||
}
|
||||
fclose (fp);
|
||||
}
|
||||
#else
|
||||
#define debug_dump(...)
|
||||
#endif
|
||||
|
||||
// - - Optimization passes - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static size_t
|
||||
optimize_assignment (struct instruction *irb, size_t irb_len)
|
||||
{
|
||||
size_t in = 0, out = 0;
|
||||
for (; in < irb_len; in++, out++)
|
||||
{
|
||||
if (in + 2 < irb_len
|
||||
&& irb[in ].cmd == BEGIN
|
||||
&& irb[in + 1].cmd == DEC && irb[in + 1].arg == 1
|
||||
&& irb[in + 2].cmd == END)
|
||||
{
|
||||
irb[out] = INSTRUCTION (SET, 0, 0);
|
||||
in += 2;
|
||||
}
|
||||
else if (out && irb[out - 1].cmd == SET && irb[in].cmd == INC)
|
||||
irb[--out].arg += irb[in].arg;
|
||||
else if (out != in)
|
||||
irb[out] = irb[in];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Add offsets to INC/DEC/SET stuck between LEFT/RIGHT
|
||||
// and compress the LEFT/RIGHT sequences
|
||||
static size_t
|
||||
optimize_offseted_inc_dec (struct instruction *irb, size_t irb_len)
|
||||
{
|
||||
size_t in = 0, out = 0;
|
||||
for (in = 0, out = 0; in < irb_len; in++, out++)
|
||||
{
|
||||
intptr_t dir = 0;
|
||||
if (irb[in].cmd == RIGHT)
|
||||
dir = irb[in].arg;
|
||||
else if (irb[in].cmd == LEFT)
|
||||
dir = -(intptr_t) irb[in].arg;
|
||||
else
|
||||
{
|
||||
irb[out] = irb[in];
|
||||
continue;
|
||||
}
|
||||
|
||||
while (in + 2 < irb_len)
|
||||
{
|
||||
// An immediate offset has its limits on x86-64
|
||||
if (dir < INT8_MIN || dir > INT8_MAX)
|
||||
break;
|
||||
|
||||
intptr_t diff;
|
||||
if (irb[in + 2].cmd == RIGHT)
|
||||
diff = irb[in + 2].arg;
|
||||
else if (irb[in + 2].cmd == LEFT)
|
||||
diff = -(intptr_t) irb[in + 2].arg;
|
||||
else
|
||||
break;
|
||||
|
||||
int cmd = irb[in + 1].cmd;
|
||||
if (cmd != INC && cmd != DEC && cmd != SET)
|
||||
break;
|
||||
|
||||
irb[out] = irb[in + 1];
|
||||
irb[out].offset = dir;
|
||||
|
||||
dir += diff;
|
||||
out += 1;
|
||||
in += 2;
|
||||
}
|
||||
|
||||
for (; in + 1 < irb_len; in++)
|
||||
{
|
||||
if (irb[in + 1].cmd == RIGHT)
|
||||
dir += irb[in + 1].arg;
|
||||
else if (irb[in + 1].cmd == LEFT)
|
||||
dir -= (intptr_t) irb[in + 1].arg;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dir)
|
||||
out--;
|
||||
else if (dir > 0)
|
||||
irb[out] = INSTRUCTION (RIGHT, 0, dir);
|
||||
else
|
||||
irb[out] = INSTRUCTION (LEFT, 0, -dir);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Try to eliminate loops that eat a cell and add/subtract its value
|
||||
// to/from some other cell
|
||||
static size_t
|
||||
optimize_inc_dec_loops (struct instruction *irb, size_t irb_len)
|
||||
{
|
||||
size_t in = 0, out = 0;
|
||||
for (in = 0, out = 0; in < irb_len; in++, out++)
|
||||
{
|
||||
irb[out] = irb[in];
|
||||
if (irb[in].cmd != BEGIN)
|
||||
continue;
|
||||
|
||||
bool ok = false;
|
||||
size_t count = 0;
|
||||
for (size_t k = in + 1; k < irb_len; k++)
|
||||
{
|
||||
if (irb[k].cmd == END)
|
||||
{
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
if (irb[k].cmd != INC
|
||||
&& irb[k].cmd != DEC)
|
||||
break;
|
||||
count++;
|
||||
}
|
||||
if (!ok)
|
||||
continue;
|
||||
|
||||
// Stable sort operations by their offsets, put [0] first
|
||||
bool sorted;
|
||||
do
|
||||
{
|
||||
sorted = true;
|
||||
for (size_t k = 1; k < count; k++)
|
||||
{
|
||||
if (irb[in + k].offset == 0)
|
||||
continue;
|
||||
if (irb[in + k + 1].offset != 0
|
||||
&& irb[in + k].offset <= irb[in + k + 1].offset)
|
||||
continue;
|
||||
|
||||
struct instruction tmp = irb[in + k + 1];
|
||||
irb[in + k + 1] = irb[in + k];
|
||||
irb[in + k] = tmp;
|
||||
sorted = false;
|
||||
}
|
||||
}
|
||||
while (!sorted);
|
||||
|
||||
// Abort the optimization on duplicate offsets (complication with [0])
|
||||
for (size_t k = 1; k < count; k++)
|
||||
if (irb[in + k].offset == irb[in + k + 1].offset)
|
||||
ok = false;
|
||||
// XXX: can't make the code longer either
|
||||
for (size_t k = 1; k <= count; k++)
|
||||
if (irb[in + k].arg != 1)
|
||||
ok = false;
|
||||
if (!ok
|
||||
|| irb[in + 1].cmd != DEC
|
||||
|| irb[in + 1].offset != 0)
|
||||
continue;
|
||||
|
||||
int min_safe_left_offset = 0;
|
||||
if (in > 1 && irb[in - 1].cmd == RIGHT)
|
||||
min_safe_left_offset = -irb[in - 1].arg;
|
||||
|
||||
bool cond_needed_for_safety = false;
|
||||
for (size_t k = 0; k < count; k++)
|
||||
if (irb[in + k + 1].offset < min_safe_left_offset)
|
||||
{
|
||||
cond_needed_for_safety = true;
|
||||
break;
|
||||
}
|
||||
|
||||
in++;
|
||||
if (cond_needed_for_safety)
|
||||
out++;
|
||||
|
||||
irb[out] = INSTRUCTION (EAT, 0, 0);
|
||||
for (size_t k = 1; k < count; k++)
|
||||
irb[out + k] = INSTRUCTION (irb[in + k].cmd == INC
|
||||
? INCACC : DECACC, irb[in + k].offset, 0);
|
||||
|
||||
in += count;
|
||||
out += count;
|
||||
|
||||
if (cond_needed_for_safety)
|
||||
irb[out] = INSTRUCTION (END, 0, 0);
|
||||
else
|
||||
out--;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// - - Loop pairing - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static void
|
||||
pair_loops (struct instruction *irb, size_t irb_len)
|
||||
{
|
||||
size_t nesting = 0;
|
||||
size_t *stack = xcalloc (sizeof *stack, irb_len);
|
||||
for (size_t i = 0; i < irb_len; i++)
|
||||
{
|
||||
switch (irb[i].cmd)
|
||||
{
|
||||
case BEGIN:
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case END:
|
||||
if (nesting <= 0)
|
||||
exit_fatal ("unbalanced loops\n");
|
||||
|
||||
--nesting;
|
||||
irb[stack[nesting]].arg = i + 1;
|
||||
|
||||
// Looping can be disabled by optimizations
|
||||
if (irb[i].arg)
|
||||
irb[i].arg = stack[nesting] + 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
free (stack);
|
||||
|
||||
if (nesting != 0)
|
||||
exit_fatal ("unbalanced loops\n");
|
||||
}
|
||||
|
||||
// - - Main - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
if (argc > 3)
|
||||
exit_fatal ("usage: %s [INPUT-FILE]\n", argv[0]);
|
||||
|
||||
FILE *input_file = stdin;
|
||||
if (argc > 1 && !(input_file = fopen (argv[1], "r")))
|
||||
exit_fatal ("fopen: %s: %s\n", argv[1], strerror (errno));
|
||||
|
||||
const char *output_path = "a.out";
|
||||
if (argc > 2)
|
||||
output_path = argv[2];
|
||||
|
||||
struct str buffer;
|
||||
str_init (&buffer);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (input_file)) != EOF)
|
||||
str_append_c (&buffer, c);
|
||||
if (ferror (input_file))
|
||||
exit_fatal ("can't read program\n");
|
||||
fclose (input_file);
|
||||
|
||||
// - - Decode, group and optimize - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// This is our Intermediate Representation Buffer
|
||||
struct instruction *irb = xcalloc (sizeof *irb, buffer.len);
|
||||
size_t irb_len = 0;
|
||||
|
||||
for (size_t i = 0; i < buffer.len; i++)
|
||||
{
|
||||
enum command cmd;
|
||||
switch (buffer.str[i])
|
||||
{
|
||||
case '>': cmd = RIGHT; break;
|
||||
case '<': cmd = LEFT; break;
|
||||
case '+': cmd = INC; break;
|
||||
case '-': cmd = DEC; break;
|
||||
case '.': cmd = OUT; break;
|
||||
case ',': cmd = IN; break;
|
||||
case '[': cmd = BEGIN; break;
|
||||
case ']': cmd = END; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
// The most basic optimization is to group identical commands together
|
||||
if (!irb_len || !grouped[cmd] || irb[irb_len - 1].cmd != cmd)
|
||||
irb_len++;
|
||||
|
||||
irb[irb_len - 1].cmd = cmd;
|
||||
irb[irb_len - 1].arg++;
|
||||
}
|
||||
|
||||
debug_dump ("bf-no-opt.txt", irb, irb_len);
|
||||
irb_len = optimize_assignment (irb, irb_len);
|
||||
debug_dump ("bf-pre-offsets.txt", irb, irb_len);
|
||||
irb_len = optimize_offseted_inc_dec (irb, irb_len);
|
||||
debug_dump ("bf-pre-incdec-unloop.txt", irb, irb_len);
|
||||
irb_len = optimize_inc_dec_loops (irb, irb_len);
|
||||
debug_dump ("bf-optimized.txt", irb, irb_len);
|
||||
pair_loops (irb, irb_len);
|
||||
debug_dump ("bf-final.txt", irb, irb_len);
|
||||
|
||||
// - - Code generation - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
str_init (&buffer);
|
||||
size_t *offsets = xcalloc (sizeof *offsets, irb_len + 1);
|
||||
bool *sets_flags = xcalloc (sizeof *sets_flags, irb_len);
|
||||
|
||||
#define CODE(x) { char t[] = x; str_append_data (&buffer, t, sizeof t - 1); }
|
||||
#define LE(v) (uint8_t[]) { v, v>>8, v>>16, v>>24, v>>32, v>>40, v>>48, v>>56 }
|
||||
#define DB(x) { uint64_t v = (x); str_append_data (&buffer, LE (v), 1); }
|
||||
#define DW(x) { uint64_t v = (x); str_append_data (&buffer, LE (v), 2); }
|
||||
#define DD(x) { uint64_t v = (x); str_append_data (&buffer, LE (v), 4); }
|
||||
#define DQ(x) { uint64_t v = (x); str_append_data (&buffer, LE (v), 8); }
|
||||
|
||||
enum
|
||||
{
|
||||
ELF_LOAD_CODE = 0x400000, // where code is loaded (usual)
|
||||
ELF_LOAD_DATA = 0x800000 // where the tape is placed
|
||||
};
|
||||
|
||||
CODE ("\xB8") DD (ELF_LOAD_DATA) // mov rax, "ELF_LOAD_DATA"
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
|
||||
for (size_t i = 0; i < irb_len; i++)
|
||||
{
|
||||
offsets[i] = buffer.len;
|
||||
|
||||
size_t arg = irb[i].arg;
|
||||
assert (arg <= UINT32_MAX);
|
||||
|
||||
int offset = irb[i].offset;
|
||||
assert (offset <= INT8_MAX && offset >= INT8_MIN);
|
||||
|
||||
// Don't save what we've just loaded
|
||||
if (irb[i].cmd == LEFT || irb[i].cmd == RIGHT)
|
||||
if (i < 2 || i + 1 >= irb_len
|
||||
|| (irb[i - 2].cmd != LEFT && irb[i - 2].cmd != RIGHT)
|
||||
|| irb[i - 1].cmd != BEGIN
|
||||
|| irb[i + 1].cmd != END)
|
||||
CODE ("\x88\x18") // mov [rax], bl
|
||||
|
||||
switch (irb[i].cmd)
|
||||
{
|
||||
case RIGHT:
|
||||
// add rax, "arg" -- optimistic, no boundary checking
|
||||
if (arg > INT8_MAX) { CODE ("\x48\x05") DD (arg) }
|
||||
else { CODE ("\x48\x83\xC0") DB (arg) }
|
||||
break;
|
||||
case LEFT:
|
||||
// sub rax, "arg" -- optimistic, no boundary checking
|
||||
if (arg > INT8_MAX) { CODE ("\x48\x2D") DD (arg) }
|
||||
else { CODE ("\x48\x83\xE8") DB (arg) }
|
||||
break;
|
||||
|
||||
case EAT:
|
||||
// NOTE: the kernel destroys rcx and r11 on syscalls,
|
||||
// there must be no OUT or IN between EAT and INCACC/DECACC
|
||||
CODE ("\x88\xD9" "\x30\xDB") // mov cl, bl; xor bl, bl
|
||||
sets_flags[i] = true;
|
||||
break;
|
||||
case INCACC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x00\x48") DB (offset) // add [rax+"offset"], cl
|
||||
}
|
||||
else
|
||||
{
|
||||
CODE ("\x00\xCB") // add bl, cl
|
||||
sets_flags[i] = true;
|
||||
}
|
||||
break;
|
||||
case DECACC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x28\x48") DB (offset) // sub [rax+"offset"], cl
|
||||
}
|
||||
else
|
||||
{
|
||||
CODE ("\x28\xCB") // sub bl, cl
|
||||
sets_flags[i] = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case INC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x80\x40") DB (offset) // add byte [rax+"offset"], "arg"
|
||||
}
|
||||
else
|
||||
{
|
||||
CODE ("\x80\xC3") // add bl, "arg"
|
||||
sets_flags[i] = true;
|
||||
}
|
||||
DB (arg)
|
||||
break;
|
||||
case DEC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x80\x68") DB (offset) // sub byte [rax+"offset"], "arg"
|
||||
}
|
||||
else
|
||||
{
|
||||
CODE ("\x80\xEB") // sub bl, "arg"
|
||||
sets_flags[i] = true;
|
||||
}
|
||||
DB (arg)
|
||||
break;
|
||||
case SET:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\xC6\x40") DB (offset) // mov byte [rax+"offset"], "arg"
|
||||
}
|
||||
else
|
||||
CODE ("\xB3") // mov bl, "arg"
|
||||
DB (arg)
|
||||
break;
|
||||
|
||||
case OUT:
|
||||
CODE ("\xE8") DD (0) // call "write"
|
||||
break;
|
||||
case IN:
|
||||
CODE ("\xE8") DD (0) // call "read"
|
||||
break;
|
||||
|
||||
case BEGIN:
|
||||
// Don't test the register when the flag has been set already;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!i || !sets_flags[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x84\x00\x00\x00\x00") // jz "offsets[arg]"
|
||||
break;
|
||||
case END:
|
||||
// We know that the cell is zero, make this an "if", not a "loop";
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!arg)
|
||||
break;
|
||||
|
||||
if (!i || !sets_flags[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x85\x00\x00\x00\x00") // jnz "offsets[arg]"
|
||||
break;
|
||||
}
|
||||
|
||||
// No sense in reading it out when we overwrite it immediately;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (irb[i].cmd == LEFT || irb[i].cmd == RIGHT)
|
||||
if (i + 1 >= irb_len
|
||||
|| irb[i + 1].cmd != SET
|
||||
|| irb[i + 1].offset != 0)
|
||||
CODE ("\x8A\x18") // mov bl, [rax]
|
||||
}
|
||||
// When there is a loop at the end we need to be able to jump past it
|
||||
offsets[irb_len] = buffer.len;
|
||||
|
||||
// Write an epilog which handles all the OS interfacing
|
||||
//
|
||||
// System V x86-64 ABI:
|
||||
// rax <-> both syscall number and return value
|
||||
// args -> rdi, rsi, rdx, r10, r8, r9
|
||||
// trashed <- rcx, r11
|
||||
|
||||
enum { SYS_READ = 0, SYS_WRITE = 1, SYS_EXIT = 60 };
|
||||
|
||||
CODE ("\xB8") DD (SYS_EXIT) // mov eax, 0x3c
|
||||
CODE ("\x48\x31\xFF") // xor rdi, rdi
|
||||
CODE ("\x0F\x05") // syscall
|
||||
|
||||
size_t fatal_offset = buffer.len;
|
||||
CODE ("\x48\x89\xF7") // mov rdi, rsi -- use the string in rsi
|
||||
CODE ("\x30\xC0") // xor al, al -- look for the nil byte
|
||||
CODE ("\x48\x31\xC9") // xor rcx, rcx
|
||||
CODE ("\x48\xF7\xD1") // not rcx -- start from -1
|
||||
CODE ("\xFC" "\xF2\xAE") // cld; repne scasb -- decrement until found
|
||||
CODE ("\x48\xF7\xD1") // not rcx
|
||||
CODE ("\x48\x8D\x51\xFF") // lea rdx, [rcx-1] -- save length in rdx
|
||||
CODE ("\xB8") DD (SYS_WRITE) // mov eax, "SYS_WRITE"
|
||||
CODE ("\xBF") DD (2) // mov edi, "STDERR_FILENO"
|
||||
CODE ("\x0F\x05") // syscall
|
||||
|
||||
CODE ("\xB8") DD (SYS_EXIT) // mov eax, "SYS_EXIT"
|
||||
CODE ("\xBF") DD (1) // mov edi, "EXIT_FAILURE"
|
||||
CODE ("\x0F\x05") // syscall
|
||||
|
||||
size_t read_offset = buffer.len;
|
||||
CODE ("\x50") // push rax -- save tape position
|
||||
CODE ("\xB8") DD (SYS_READ) // mov eax, "SYS_READ"
|
||||
CODE ("\x48\x89\xC7") // mov rdi, rax -- STDIN_FILENO
|
||||
CODE ("\x66\x6A\x00") // push word 0 -- the default value for EOF
|
||||
CODE ("\x48\x89\xE6") // mov rsi, rsp -- the char starts at rsp
|
||||
CODE ("\xBA") DD (1) // mov edx, 1 -- count
|
||||
CODE ("\x0F\x05") // syscall
|
||||
CODE ("\x66\x5B") // pop bx
|
||||
|
||||
CODE ("\x48\x83\xF8\x00") // cmp rax, 0
|
||||
CODE ("\x48\x8D\x35") DD (4) // lea rsi, [rel read_message]
|
||||
CODE ("\x7C") // jl "fatal_offset" -- write failure message
|
||||
DB ((intptr_t) fatal_offset - (intptr_t) (buffer.len + 1))
|
||||
CODE ("\x58") // pop rax -- restore tape position
|
||||
CODE ("\xC3") // ret
|
||||
CODE ("fatal: read failed\n\0")
|
||||
|
||||
size_t write_offset = buffer.len;
|
||||
CODE ("\x50") // push rax -- save tape position
|
||||
CODE ("\xB8") DD (SYS_WRITE) // mov eax, "SYS_WRITE"
|
||||
CODE ("\x48\x89\xC7") // mov rdi, rax -- STDOUT_FILENO
|
||||
CODE ("\x66\x53") // push bx
|
||||
CODE ("\x48\x89\xE6") // mov rsi, rsp -- the char starts at rsp
|
||||
CODE ("\xBA") DD (1) // mov edx, 1 -- count
|
||||
CODE ("\x0F\x05") // syscall
|
||||
CODE ("\x66\x5B") // pop bx
|
||||
|
||||
CODE ("\x48\x83\xF8\x00") // cmp rax, 0
|
||||
CODE ("\x48\x8D\x35") DD (4) // lea rsi, [rel write_message]
|
||||
CODE ("\x7C") // jl "fatal_offset" -- write failure message
|
||||
DB ((intptr_t) fatal_offset - (intptr_t) (buffer.len + 1))
|
||||
CODE ("\x58") // pop rax -- restore tape position
|
||||
CODE ("\xC3") // ret
|
||||
CODE ("fatal: write failed\n\0")
|
||||
|
||||
// Now that we know where each instruction is, fill in relative jumps
|
||||
for (size_t i = 0; i < irb_len; i++)
|
||||
{
|
||||
if (!irb[i].arg)
|
||||
continue;
|
||||
|
||||
// This must accurately reflect the code generators
|
||||
intptr_t target, fixup = offsets[i];
|
||||
if (irb[i].cmd == BEGIN || irb[i].cmd == END)
|
||||
{
|
||||
fixup += (i && sets_flags[i - 1]) ? 2 : 4;
|
||||
target = offsets[irb[i].arg];
|
||||
}
|
||||
else if (irb[i].cmd == IN) { fixup++; target = read_offset; }
|
||||
else if (irb[i].cmd == OUT) { fixup++; target = write_offset; }
|
||||
else continue;
|
||||
|
||||
uint64_t v = target - (fixup + 4);
|
||||
memcpy (buffer.str + fixup, LE (v), 4);
|
||||
}
|
||||
free (offsets);
|
||||
free (sets_flags);
|
||||
|
||||
// - - Output - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Now that we know how long the machine code is, we can write the header.
|
||||
// Note that for PIE we would need to depend on the dynamic linker, so no.
|
||||
//
|
||||
// Recommended reading:
|
||||
// http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html
|
||||
// man 5 elf
|
||||
|
||||
struct str code = buffer;
|
||||
str_init (&buffer);
|
||||
|
||||
enum
|
||||
{
|
||||
ELF_HEADER_SIZE = 64, // size of the ELF header
|
||||
ELF_PROGRAM_ENTRY_SIZE = 56, // size of a program header
|
||||
ELF_META_SIZE = ELF_HEADER_SIZE + 2 * ELF_PROGRAM_ENTRY_SIZE
|
||||
};
|
||||
|
||||
// ELF header
|
||||
CODE ("\x7F" "ELF\x02\x01\x01") // ELF, 64-bit, little endian, v1
|
||||
CODE ("\x00\x00" "\0\0\0\0\0\0\0") // Unix System V ABI, v0, padding
|
||||
DW (2) DW (62) DD (1) // executable, x86-64, v1
|
||||
DQ (ELF_LOAD_CODE + ELF_META_SIZE) // entry point address
|
||||
DQ (ELF_HEADER_SIZE) DQ (0) // program, section header offset
|
||||
DD (0) // no processor-specific flags
|
||||
DW (ELF_HEADER_SIZE) // ELF header size
|
||||
DW (ELF_PROGRAM_ENTRY_SIZE) DW (2) // program hdr tbl entry size, count
|
||||
DW (0) DW (0) // section hdr tbl entry size, count
|
||||
DW (0) // no section index for strings
|
||||
|
||||
// Program header for code
|
||||
// The entry point address seems to require alignment, so map start of file
|
||||
DD (1) DD (5) // PT_LOAD, PF_R | PF_X
|
||||
DQ (0) // offset within the file
|
||||
DQ (ELF_LOAD_CODE) // address in virtual memory
|
||||
DQ (ELF_LOAD_CODE) // address in physical memory
|
||||
DQ (code.len + ELF_META_SIZE) // length within the file
|
||||
DQ (code.len + ELF_META_SIZE) // length within memory
|
||||
DQ (4096) // segment alignment
|
||||
|
||||
// Program header for the tape
|
||||
DD (1) DD (6) // PT_LOAD, PF_R | PF_W
|
||||
DQ (0) // offset within the file
|
||||
DQ (ELF_LOAD_DATA) // address in virtual memory
|
||||
DQ (ELF_LOAD_DATA) // address in physical memory
|
||||
DQ (0) // length within the file
|
||||
DQ (1 << 20) // one megabyte of memory
|
||||
DQ (4096) // segment alignment
|
||||
|
||||
// The section header table is optional and we don't need it for anything
|
||||
|
||||
FILE *output_file;
|
||||
if (!(output_file = fopen (output_path, "w")))
|
||||
exit_fatal ("fopen: %s: %s\n", output_path, strerror (errno));
|
||||
|
||||
fwrite (buffer.str, buffer.len, 1, output_file);
|
||||
fwrite (code.str, code.len, 1, output_file);
|
||||
fclose (output_file);
|
||||
return 0;
|
||||
}
|
14
interpreters/Makefile
Normal file
14
interpreters/Makefile
Normal file
@ -0,0 +1,14 @@
|
||||
CC = c99
|
||||
CFLAGS = -O3
|
||||
|
||||
NAMES = bf bf-faster-loops bf-optimizing \
|
||||
bf-jit bf-jit-opt bf-jit-unsafe bf-jit-unsafe-opt
|
||||
|
||||
all: $(NAMES)
|
||||
|
||||
%: %.c
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@
|
||||
clean:
|
||||
rm -f $(NAMES)
|
||||
|
||||
.PHONY: all clean
|
15
interpreters/README.adoc
Normal file
15
interpreters/README.adoc
Normal file
@ -0,0 +1,15 @@
|
||||
This directory contains several Brainfuck interpreters in various states of
|
||||
sophistication, from the simplest approach to an optimizing JIT compiler:
|
||||
|
||||
* `bf.c` is the stupidest one and the oldest by far
|
||||
* `bf-faster-loops.c` precomputes loop jumps
|
||||
* `bf-optimizing.c` improves on that by changing `[-]+` loops into assignments
|
||||
* `bf-jit.c` adds JIT compilation for Intel x86-64
|
||||
* `bf-jit-opt.c` tries a bit harder to avoid looping on the current value
|
||||
* `bf-jit-unsafe.c` abolishes all boundary checks when moving across the tape
|
||||
* `bf-jit-unsafe-opt.c` makes use of immediate offsets to modify values
|
||||
|
||||
I recommend using a tool such as 'meld' to view the differences.
|
||||
|
||||
Just run `make` in this directory to have them all built, and append
|
||||
`CPPFLAGS=-DDEBUG` to get dumps of the IR for the more sophisticated JITs.
|
151
interpreters/bf-faster-loops.c
Normal file
151
interpreters/bf-faster-loops.c
Normal file
@ -0,0 +1,151 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xmalloc (size_t n)
|
||||
{
|
||||
void *p = malloc (n);
|
||||
if (!p)
|
||||
exit_fatal ("malloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->alloc = 16;
|
||||
self->len = 0;
|
||||
self->str = strcpy (xmalloc (self->alloc), "");
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Main --------------------------------------------------------------------
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
struct str program; str_init (&program);
|
||||
struct str data; str_init (&data);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
|
||||
FILE *input = fopen ("/dev/tty", "rb");
|
||||
if (!input)
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
size_t *pairs = xmalloc (sizeof *pairs * program.len);
|
||||
size_t *stack = xmalloc (sizeof *stack * program.len);
|
||||
|
||||
size_t nesting = 0;
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '[':
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case ']':
|
||||
assert (nesting > 0);
|
||||
|
||||
--nesting;
|
||||
pairs[stack[nesting]] = i;
|
||||
pairs[i] = stack[nesting];
|
||||
}
|
||||
}
|
||||
assert (nesting == 0);
|
||||
|
||||
size_t dataptr = 0;
|
||||
str_append_c (&data, 0);
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '>':
|
||||
assert (dataptr != SIZE_MAX);
|
||||
if (++dataptr == data.len)
|
||||
str_append_c (&data, 0);
|
||||
break;
|
||||
case '<':
|
||||
assert (dataptr != 0);
|
||||
dataptr--;
|
||||
break;
|
||||
|
||||
case '+': data.str[dataptr]++; break;
|
||||
case '-': data.str[dataptr]--; break;
|
||||
|
||||
case '.':
|
||||
fputc (data.str[dataptr], stdout);
|
||||
break;
|
||||
case ',':
|
||||
data.str[dataptr] = c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
break;
|
||||
|
||||
case '[': if (!data.str[dataptr]) i = pairs[i]; break;
|
||||
case ']': if ( data.str[dataptr]) i = pairs[i]; break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
495
interpreters/bf-jit-opt.c
Normal file
495
interpreters/bf-jit-opt.c
Normal file
@ -0,0 +1,495 @@
|
||||
// This is an exercise in futility more than anything else
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#if (defined __x86_64__ || defined __amd64__) && defined __unix__
|
||||
#include <sys/mman.h>
|
||||
#else
|
||||
#error Platform not supported
|
||||
#endif
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xcalloc (size_t m, size_t n)
|
||||
{
|
||||
void *p = calloc (m, n);
|
||||
if (!p)
|
||||
exit_fatal ("calloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->len = 0;
|
||||
self->str = xcalloc (1, (self->alloc = 16));
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Application -------------------------------------------------------------
|
||||
|
||||
struct str data; ///< Data tape
|
||||
volatile size_t dataptr; ///< Current location on the tape
|
||||
FILE *input; ///< User input
|
||||
|
||||
enum command { RIGHT, LEFT, INC, DEC, SET, IN, OUT, BEGIN, END,
|
||||
EAT, INCACC, DECACC };
|
||||
bool grouped[] = { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 };
|
||||
struct instruction { enum command cmd; size_t arg; };
|
||||
#define INSTRUCTION(c, a) (struct instruction) { (c), (a) }
|
||||
|
||||
// - - Callbacks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Some things I just really don't want to write in assembly even though it
|
||||
// is effectively a big performance hit, eliminating the advantage of JIT
|
||||
|
||||
static void
|
||||
right (size_t arg)
|
||||
{
|
||||
assert (SIZE_MAX - dataptr > arg);
|
||||
dataptr += arg;
|
||||
|
||||
while (dataptr >= data.len)
|
||||
str_append_c (&data, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
left (size_t arg)
|
||||
{
|
||||
assert (dataptr >= arg);
|
||||
dataptr -= arg;
|
||||
}
|
||||
|
||||
static void
|
||||
cin (void)
|
||||
{
|
||||
int c;
|
||||
data.str[dataptr] = c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
}
|
||||
|
||||
// - - Main - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
#ifdef DEBUG
|
||||
static void
|
||||
debug_dump (const char *filename, struct instruction *in, size_t len)
|
||||
{
|
||||
FILE *fp = fopen (filename, "w");
|
||||
long indent = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (in[i].cmd == END)
|
||||
indent--;
|
||||
for (long k = 0; k < indent; k++)
|
||||
fprintf (fp, " ");
|
||||
|
||||
switch (in[i].cmd)
|
||||
{
|
||||
case RIGHT: fprintf (fp, "RIGHT %zu\n", in[i].arg); break;
|
||||
case LEFT: fprintf (fp, "LEFT %zu\n", in[i].arg); break;
|
||||
case INC: fprintf (fp, "INC %zu\n", in[i].arg); break;
|
||||
case DEC: fprintf (fp, "DEC %zu\n", in[i].arg); break;
|
||||
case OUT: fprintf (fp, "OUT %zu\n", in[i].arg); break;
|
||||
case IN: fprintf (fp, "IN %zu\n", in[i].arg); break;
|
||||
case BEGIN: fprintf (fp, "BEGIN %zu\n", in[i].arg); break;
|
||||
case END: fprintf (fp, "END %zu\n", in[i].arg); break;
|
||||
case SET: fprintf (fp, "SET %zu\n", in[i].arg); break;
|
||||
case EAT: fprintf (fp, "EAT %zu\n", in[i].arg); break;
|
||||
case INCACC: fprintf (fp, "INCACC %zu\n", in[i].arg); break;
|
||||
case DECACC: fprintf (fp, "DECACC %zu\n", in[i].arg); break;
|
||||
}
|
||||
if (in[i].cmd == BEGIN)
|
||||
indent++;
|
||||
}
|
||||
fclose (fp);
|
||||
}
|
||||
#else
|
||||
#define debug_dump(...)
|
||||
#endif
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
struct str program;
|
||||
str_init (&program);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
if (!(input = fopen ("/dev/tty", "rb")))
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
// - - Decode and group - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct instruction *parsed = xcalloc (sizeof *parsed, program.len);
|
||||
size_t parsed_len = 0;
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
enum command cmd;
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '>': cmd = RIGHT; break;
|
||||
case '<': cmd = LEFT; break;
|
||||
case '+': cmd = INC; break;
|
||||
case '-': cmd = DEC; break;
|
||||
case '.': cmd = OUT; break;
|
||||
case ',': cmd = IN; break;
|
||||
case '[': cmd = BEGIN; break;
|
||||
case ']': cmd = END; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
// The most basic optimization is to group identical commands together
|
||||
if (!parsed_len || !grouped[cmd] || parsed[parsed_len - 1].cmd != cmd)
|
||||
parsed_len++;
|
||||
|
||||
parsed[parsed_len - 1].cmd = cmd;
|
||||
parsed[parsed_len - 1].arg++;
|
||||
}
|
||||
|
||||
// - - Optimization passes - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
debug_dump ("bf-no-opt.txt", parsed, parsed_len);
|
||||
|
||||
size_t in = 0, out = 0;
|
||||
for (; in < parsed_len; in++, out++)
|
||||
{
|
||||
// This shows up in mandelbrot.bf a lot but actually helps hanoi.bf
|
||||
if (in + 5 < parsed_len
|
||||
&& parsed[in].cmd == BEGIN && parsed[in + 5].cmd == END
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
|
||||
&& parsed[in + 2].cmd == LEFT && parsed[in + 4].cmd == RIGHT
|
||||
&& parsed[in + 2].arg == parsed[in + 4].arg
|
||||
|
||||
&& (parsed[in + 3].cmd == INC || parsed[in + 3].cmd == DEC)
|
||||
&& parsed[in + 3].arg == 1)
|
||||
{
|
||||
// This mustn't make the move when the cell is zero already
|
||||
parsed[out] = parsed[in];
|
||||
parsed[out + 1] = INSTRUCTION (EAT, 0);
|
||||
parsed[out + 2] = parsed[in + 2];
|
||||
parsed[out + 3] = INSTRUCTION
|
||||
(parsed[in + 3].cmd == INC ? INCACC : DECACC, 0);
|
||||
parsed[out + 4] = parsed[in + 4];
|
||||
// This disables the looping further in the code;
|
||||
// this doesn't have much of an effect in practice
|
||||
parsed[out + 5] = INSTRUCTION (END, 0);
|
||||
in += 5;
|
||||
out += 5;
|
||||
}
|
||||
// The simpler case that cannot crash and thus can avoid the loop
|
||||
else if (in + 5 < parsed_len
|
||||
&& parsed[in].cmd == BEGIN && parsed[in + 5].cmd == END
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
|
||||
&& parsed[in + 2].cmd == RIGHT && parsed[in + 4].cmd == LEFT
|
||||
&& parsed[in + 2].arg == parsed[in + 4].arg
|
||||
|
||||
&& (parsed[in + 3].cmd == INC || parsed[in + 3].cmd == DEC)
|
||||
&& parsed[in + 3].arg == 1)
|
||||
{
|
||||
parsed[out] = INSTRUCTION (EAT, 0);
|
||||
parsed[out + 1] = parsed[in + 2];
|
||||
parsed[out + 2] = INSTRUCTION
|
||||
(parsed[in + 3].cmd == INC ? INCACC : DECACC, 0);
|
||||
parsed[out + 3] = parsed[in + 4];
|
||||
in += 5;
|
||||
out += 3;
|
||||
}
|
||||
else if (in + 2 < parsed_len
|
||||
&& parsed[in ].cmd == BEGIN
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
&& parsed[in + 2].cmd == END)
|
||||
{
|
||||
parsed[out] = INSTRUCTION (SET, 0);
|
||||
in += 2;
|
||||
}
|
||||
else if (out && parsed[out - 1].cmd == SET && parsed[in].cmd == INC)
|
||||
parsed[--out].arg += parsed[in].arg;
|
||||
else if (out != in)
|
||||
parsed[out] = parsed[in];
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
for (in = 0, out = 0; in < parsed_len; in++, out++)
|
||||
{
|
||||
ssize_t dir = 0;
|
||||
if (parsed[in].cmd == RIGHT)
|
||||
dir = parsed[in].arg;
|
||||
else if (parsed[in].cmd == LEFT)
|
||||
dir = -(ssize_t) parsed[in].arg;
|
||||
else
|
||||
{
|
||||
parsed[out] = parsed[in];
|
||||
continue;
|
||||
}
|
||||
|
||||
for (; in + 1 < parsed_len; in++)
|
||||
{
|
||||
if (parsed[in + 1].cmd == RIGHT)
|
||||
dir += parsed[in + 1].arg;
|
||||
else if (parsed[in + 1].cmd == LEFT)
|
||||
dir -= (ssize_t) parsed[in + 1].arg;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dir)
|
||||
out--;
|
||||
else if (dir > 0)
|
||||
parsed[out] = INSTRUCTION (RIGHT, dir);
|
||||
else
|
||||
parsed[out] = INSTRUCTION (LEFT, -dir);
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
debug_dump ("bf-optimized.txt", parsed, parsed_len);
|
||||
|
||||
// - - Loop pairing - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t nesting = 0;
|
||||
size_t *stack = xcalloc (sizeof *stack, parsed_len);
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case BEGIN:
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case END:
|
||||
assert (nesting > 0);
|
||||
|
||||
--nesting;
|
||||
parsed[stack[nesting]].arg = i + 1;
|
||||
|
||||
// Looping can be disabled by optimizations
|
||||
if (parsed[i].arg)
|
||||
parsed[i].arg = stack[nesting] + 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
free (stack);
|
||||
assert (nesting == 0);
|
||||
|
||||
debug_dump ("bf-final.txt", parsed, parsed_len);
|
||||
|
||||
// - - JIT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Functions preserve the registers rbx, rsp, rbp, r12, r13, r14, and r15;
|
||||
// while rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 are scratch registers.
|
||||
|
||||
str_init (&program);
|
||||
size_t *offsets = xcalloc (sizeof *offsets, parsed_len + 1);
|
||||
uint8_t *arith = xcalloc (sizeof *arith, parsed_len);
|
||||
|
||||
#define CODE(x) { char t[] = x; str_append_data (&program, t, sizeof t - 1); }
|
||||
#define WORD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 8); }
|
||||
#define DWRD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 4); }
|
||||
|
||||
CODE ("\x49\xBD") WORD (&dataptr) // mov r13, qword "&dataptr"
|
||||
CODE ("\x49\xBF") WORD (&data.str) // mov r15, qword "&data.str"
|
||||
CODE ("\x4D\x8B\x37") // mov r14, qword [r15]
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
offsets[i] = program.len;
|
||||
|
||||
size_t arg = parsed[i].arg;
|
||||
assert (arg <= UINT32_MAX);
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case RIGHT:
|
||||
CODE ("\x41\x88\x1E") // mov [r14], bl
|
||||
CODE ("\xBF") DWRD (arg) // mov edi, "arg"
|
||||
CODE ("\x48\xB8") WORD (right) // mov rax, "right"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
|
||||
// The data could get reallocated, so reload the address
|
||||
CODE ("\x4D\x8B\x37") // mov r14, qword [r15]
|
||||
CODE ("\x4D\x03\x75\x00") // add r14, [r13]
|
||||
break;
|
||||
case LEFT:
|
||||
CODE ("\x41\x88\x1E") // mov [r14], bl
|
||||
CODE ("\xBF") DWRD (arg) // mov edi, "arg"
|
||||
CODE ("\x49\x29\xFE") // sub r14, rdi -- optimistic
|
||||
CODE ("\x48\xB8") WORD (left) // mov rax, "left"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
break;
|
||||
|
||||
case EAT:
|
||||
CODE ("\x41\x88\xDC") // mov r12b, bl
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case INCACC:
|
||||
CODE ("\x44\x00\xE3") // add bl, r12b
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case DECACC:
|
||||
CODE ("\x44\x28\xE3") // sub bl, r12b
|
||||
arith[i] = 1;
|
||||
break;
|
||||
|
||||
case INC:
|
||||
CODE ("\x80\xC3") // add bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case DEC:
|
||||
CODE ("\x80\xEB") // sub bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case SET:
|
||||
CODE ("\xB3") // mov bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
|
||||
case OUT:
|
||||
CODE ("\x48\x0F\xB6\xFB") // movzx rdi, bl
|
||||
CODE ("\x48\xBE") WORD (stdout) // mov rsi, "stdout"
|
||||
CODE ("\x48\xB8") WORD (fputc) // mov rax, "fputc"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
break;
|
||||
case IN:
|
||||
CODE ("\x48\xB8") WORD (cin) // mov rax, "cin"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x41\x8A\x1E") // mov bl, [r14]
|
||||
break;
|
||||
|
||||
case BEGIN:
|
||||
// Don't test the register when the flag has been set already;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!i || !arith[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x84\x00\x00\x00\x00") // jz "offsets[i]"
|
||||
break;
|
||||
case END:
|
||||
// We know that the cell is zero, make this an "if", not a "loop";
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!arg)
|
||||
break;
|
||||
|
||||
if (!i || !arith[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x85\x00\x00\x00\x00") // jnz "offsets[i]"
|
||||
break;
|
||||
}
|
||||
|
||||
// No sense in reading it out when we overwrite it immediately;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (parsed[i].cmd == LEFT || parsed[i].cmd == RIGHT)
|
||||
if (i + 1 >= parsed_len
|
||||
|| parsed[i + 1].cmd != SET)
|
||||
CODE ("\x41\x8A\x1E") // mov bl, [r14]
|
||||
}
|
||||
// When there is a loop at the end we need to be able to jump past it
|
||||
offsets[parsed_len] = program.len;
|
||||
str_append_c (&program, '\xC3'); // ret
|
||||
|
||||
// Now that we know where each instruction is, fill in relative jumps;
|
||||
// this must accurately reflect code generators for BEGIN and END
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
if ((parsed[i].cmd != BEGIN && parsed[i].cmd != END)
|
||||
|| !parsed[i].arg)
|
||||
continue;
|
||||
|
||||
size_t fixup = offsets[i] + 2;
|
||||
if (!i || !arith[i - 1])
|
||||
fixup += 2;
|
||||
|
||||
*(int32_t *)(program.str + fixup) =
|
||||
((intptr_t)(offsets[parsed[i].arg]) - (intptr_t)(fixup + 4));
|
||||
}
|
||||
free (offsets);
|
||||
free (arith);
|
||||
|
||||
#ifdef DEBUG
|
||||
FILE *bin = fopen ("bf-jit.bin", "w");
|
||||
fwrite (program.str, program.len, 1, bin);
|
||||
fclose (bin);
|
||||
#endif
|
||||
|
||||
// - - Runtime - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Some systems may have W^X
|
||||
void *executable = mmap (NULL, program.len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (!executable)
|
||||
exit_fatal ("mmap: %s\n", strerror (errno));
|
||||
|
||||
memcpy (executable, program.str, program.len);
|
||||
if (mprotect (executable, program.len, PROT_READ | PROT_EXEC))
|
||||
exit_fatal ("mprotect: %s\n", strerror (errno));
|
||||
|
||||
str_init (&data);
|
||||
str_append_c (&data, 0);
|
||||
((void (*) (void)) executable)();
|
||||
return 0;
|
||||
}
|
617
interpreters/bf-jit-unsafe-opt.c
Normal file
617
interpreters/bf-jit-unsafe-opt.c
Normal file
@ -0,0 +1,617 @@
|
||||
// This is an exercise in futility more than anything else
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#if (defined __x86_64__ || defined __amd64__) && defined __unix__
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#else
|
||||
#error Platform not supported
|
||||
#endif
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xcalloc (size_t m, size_t n)
|
||||
{
|
||||
void *p = calloc (m, n);
|
||||
if (!p)
|
||||
exit_fatal ("calloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->len = 0;
|
||||
self->str = xcalloc (1, (self->alloc = 16));
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Application -------------------------------------------------------------
|
||||
|
||||
enum command { RIGHT, LEFT, INC, DEC, SET, IN, OUT, BEGIN, END,
|
||||
EAT, INCACC, DECACC };
|
||||
bool grouped[] = { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 };
|
||||
struct instruction { enum command cmd; int offset; size_t arg; };
|
||||
#define INSTRUCTION(c, o, a) (struct instruction) { (c), (o), (a) }
|
||||
|
||||
// - - Callbacks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
FILE *input; ///< User input
|
||||
|
||||
static int
|
||||
cin (void)
|
||||
{
|
||||
int c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
return c;
|
||||
}
|
||||
|
||||
// - - Main - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
#ifdef DEBUG
|
||||
static void
|
||||
debug_dump (const char *filename, struct instruction *in, size_t len)
|
||||
{
|
||||
FILE *fp = fopen (filename, "w");
|
||||
long indent = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (in[i].cmd == END)
|
||||
indent--;
|
||||
for (long k = 0; k < indent; k++)
|
||||
fprintf (fp, " ");
|
||||
|
||||
switch (in[i].cmd)
|
||||
{
|
||||
case RIGHT: fputs ("RIGHT ", fp); break;
|
||||
case LEFT: fputs ("LEFT ", fp); break;
|
||||
case INC: fputs ("INC ", fp); break;
|
||||
case DEC: fputs ("DEC ", fp); break;
|
||||
case OUT: fputs ("OUT ", fp); break;
|
||||
case IN: fputs ("IN ", fp); break;
|
||||
case BEGIN: fputs ("BEGIN ", fp); break;
|
||||
case END: fputs ("END ", fp); break;
|
||||
case SET: fputs ("SET ", fp); break;
|
||||
case EAT: fputs ("EAT ", fp); break;
|
||||
case INCACC: fputs ("INCACC", fp); break;
|
||||
case DECACC: fputs ("DECACC", fp); break;
|
||||
}
|
||||
fprintf (fp, " %zu [%d]\n", in[i].arg, in[i].offset);
|
||||
if (in[i].cmd == BEGIN)
|
||||
indent++;
|
||||
}
|
||||
fclose (fp);
|
||||
}
|
||||
#else
|
||||
#define debug_dump(...)
|
||||
#endif
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
struct str program;
|
||||
str_init (&program);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
if (!(input = fopen ("/dev/tty", "rb")))
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
// - - Decode and group - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct instruction *parsed = xcalloc (sizeof *parsed, program.len);
|
||||
size_t parsed_len = 0;
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
enum command cmd;
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '>': cmd = RIGHT; break;
|
||||
case '<': cmd = LEFT; break;
|
||||
case '+': cmd = INC; break;
|
||||
case '-': cmd = DEC; break;
|
||||
case '.': cmd = OUT; break;
|
||||
case ',': cmd = IN; break;
|
||||
case '[': cmd = BEGIN; break;
|
||||
case ']': cmd = END; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
// The most basic optimization is to group identical commands together
|
||||
if (!parsed_len || !grouped[cmd] || parsed[parsed_len - 1].cmd != cmd)
|
||||
parsed_len++;
|
||||
|
||||
parsed[parsed_len - 1].cmd = cmd;
|
||||
parsed[parsed_len - 1].arg++;
|
||||
}
|
||||
|
||||
// - - Optimization passes - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
debug_dump ("bf-no-opt.txt", parsed, parsed_len);
|
||||
|
||||
size_t in = 0, out = 0;
|
||||
for (; in < parsed_len; in++, out++)
|
||||
{
|
||||
if (in + 2 < parsed_len
|
||||
&& parsed[in ].cmd == BEGIN
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
&& parsed[in + 2].cmd == END)
|
||||
{
|
||||
parsed[out] = INSTRUCTION (SET, 0, 0);
|
||||
in += 2;
|
||||
}
|
||||
else if (out && parsed[out - 1].cmd == SET && parsed[in].cmd == INC)
|
||||
parsed[--out].arg += parsed[in].arg;
|
||||
else if (out != in)
|
||||
parsed[out] = parsed[in];
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
debug_dump ("bf-pre-offsets.txt", parsed, parsed_len);
|
||||
|
||||
// Add offsets to INC/DEC/SET stuck between LEFT/RIGHT
|
||||
// and compress the LEFT/RIGHT sequences
|
||||
for (in = 0, out = 0; in < parsed_len; in++, out++)
|
||||
{
|
||||
ssize_t dir = 0;
|
||||
if (parsed[in].cmd == RIGHT)
|
||||
dir = parsed[in].arg;
|
||||
else if (parsed[in].cmd == LEFT)
|
||||
dir = -(ssize_t) parsed[in].arg;
|
||||
else
|
||||
{
|
||||
parsed[out] = parsed[in];
|
||||
continue;
|
||||
}
|
||||
|
||||
while (in + 2 < parsed_len)
|
||||
{
|
||||
// An immediate offset has its limits
|
||||
if (dir < INT8_MIN || dir > INT8_MAX)
|
||||
break;
|
||||
|
||||
ssize_t diff;
|
||||
if (parsed[in + 2].cmd == RIGHT)
|
||||
diff = parsed[in + 2].arg;
|
||||
else if (parsed[in + 2].cmd == LEFT)
|
||||
diff = -(ssize_t) parsed[in + 2].arg;
|
||||
else
|
||||
break;
|
||||
|
||||
int cmd = parsed[in + 1].cmd;
|
||||
if (cmd != INC && cmd != DEC && cmd != SET)
|
||||
break;
|
||||
|
||||
parsed[out] = parsed[in + 1];
|
||||
parsed[out].offset = dir;
|
||||
|
||||
dir += diff;
|
||||
out += 1;
|
||||
in += 2;
|
||||
}
|
||||
|
||||
for (; in + 1 < parsed_len; in++)
|
||||
{
|
||||
if (parsed[in + 1].cmd == RIGHT)
|
||||
dir += parsed[in + 1].arg;
|
||||
else if (parsed[in + 1].cmd == LEFT)
|
||||
dir -= (ssize_t) parsed[in + 1].arg;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dir)
|
||||
out--;
|
||||
else if (dir > 0)
|
||||
parsed[out] = INSTRUCTION (RIGHT, 0, dir);
|
||||
else
|
||||
parsed[out] = INSTRUCTION (LEFT, 0, -dir);
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
debug_dump ("bf-pre-incdec-unloop.txt", parsed, parsed_len);
|
||||
|
||||
// Try to eliminate loops that eat a cell and add/subtract its value
|
||||
// to/from some other cell
|
||||
for (in = 0, out = 0; in < parsed_len; in++, out++)
|
||||
{
|
||||
parsed[out] = parsed[in];
|
||||
if (parsed[in].cmd != BEGIN)
|
||||
continue;
|
||||
|
||||
bool ok = false;
|
||||
size_t count = 0;
|
||||
for (size_t k = in + 1; k < parsed_len; k++)
|
||||
{
|
||||
if (parsed[k].cmd == END)
|
||||
{
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
if (parsed[k].cmd != INC
|
||||
&& parsed[k].cmd != DEC)
|
||||
break;
|
||||
count++;
|
||||
}
|
||||
if (!ok)
|
||||
continue;
|
||||
|
||||
// Stable sort operations by their offsets, put [0] first
|
||||
bool sorted;
|
||||
do
|
||||
{
|
||||
sorted = true;
|
||||
for (size_t k = 1; k < count; k++)
|
||||
{
|
||||
if (parsed[in + k].offset == 0)
|
||||
continue;
|
||||
if (parsed[in + k + 1].offset != 0
|
||||
&& parsed[in + k].offset <= parsed[in + k + 1].offset)
|
||||
continue;
|
||||
|
||||
struct instruction tmp = parsed[in + k + 1];
|
||||
parsed[in + k + 1] = parsed[in + k];
|
||||
parsed[in + k] = tmp;
|
||||
sorted = false;
|
||||
}
|
||||
}
|
||||
while (!sorted);
|
||||
|
||||
// Abort the optimization on duplicate offsets (complication with [0])
|
||||
for (size_t k = 1; k < count; k++)
|
||||
if (parsed[in + k].offset == parsed[in + k + 1].offset)
|
||||
ok = false;
|
||||
// XXX: can't make the code longer either
|
||||
for (size_t k = 1; k <= count; k++)
|
||||
if (parsed[in + k].arg != 1)
|
||||
ok = false;
|
||||
if (!ok
|
||||
|| parsed[in + 1].cmd != DEC
|
||||
|| parsed[in + 1].offset != 0)
|
||||
continue;
|
||||
|
||||
int min_safe_left_offset = 0;
|
||||
if (in > 1 && parsed[in - 1].cmd == RIGHT)
|
||||
min_safe_left_offset = -parsed[in - 1].arg;
|
||||
|
||||
bool cond_needed_for_safety = false;
|
||||
for (size_t k = 0; k < count; k++)
|
||||
if (parsed[in + k + 1].offset < min_safe_left_offset)
|
||||
{
|
||||
cond_needed_for_safety = true;
|
||||
break;
|
||||
}
|
||||
|
||||
in++;
|
||||
if (cond_needed_for_safety)
|
||||
out++;
|
||||
|
||||
parsed[out] = INSTRUCTION (EAT, 0, 0);
|
||||
for (size_t k = 1; k < count; k++)
|
||||
parsed[out + k] = INSTRUCTION (parsed[in + k].cmd == INC
|
||||
? INCACC : DECACC, parsed[in + k].offset, 0);
|
||||
|
||||
in += count;
|
||||
out += count;
|
||||
|
||||
if (cond_needed_for_safety)
|
||||
parsed[out] = INSTRUCTION (END, 0, 0);
|
||||
else
|
||||
out--;
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
debug_dump ("bf-optimized.txt", parsed, parsed_len);
|
||||
|
||||
// - - Loop pairing - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t nesting = 0;
|
||||
size_t *stack = xcalloc (sizeof *stack, parsed_len);
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case BEGIN:
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case END:
|
||||
assert (nesting > 0);
|
||||
|
||||
--nesting;
|
||||
parsed[stack[nesting]].arg = i + 1;
|
||||
|
||||
// Looping can be disabled by optimizations
|
||||
if (parsed[i].arg)
|
||||
parsed[i].arg = stack[nesting] + 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
free (stack);
|
||||
assert (nesting == 0);
|
||||
|
||||
debug_dump ("bf-final.txt", parsed, parsed_len);
|
||||
|
||||
// - - JIT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Functions preserve the registers rbx, rsp, rbp, r12, r13, r14, and r15;
|
||||
// while rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 are scratch registers.
|
||||
|
||||
str_init (&program);
|
||||
size_t *offsets = xcalloc (sizeof *offsets, parsed_len + 1);
|
||||
uint8_t *arith = xcalloc (sizeof *arith, parsed_len);
|
||||
|
||||
#define CODE(x) { char t[] = x; str_append_data (&program, t, sizeof t - 1); }
|
||||
#define WORD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 8); }
|
||||
#define DWRD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 4); }
|
||||
|
||||
CODE ("\x48\x89\xF8") // mov rax, rdi
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
offsets[i] = program.len;
|
||||
|
||||
size_t arg = parsed[i].arg;
|
||||
assert (arg <= UINT32_MAX);
|
||||
|
||||
int offset = parsed[i].offset;
|
||||
assert (offset <= INT8_MAX && offset >= INT8_MIN);
|
||||
|
||||
// Don't save what we've just loaded
|
||||
if (parsed[i].cmd == LEFT || parsed[i].cmd == RIGHT)
|
||||
if (i < 2 || i + 1 >= parsed_len
|
||||
|| (parsed[i - 2].cmd != LEFT && parsed[i - 2].cmd != RIGHT)
|
||||
|| parsed[i - 1].cmd != BEGIN
|
||||
|| parsed[i + 1].cmd != END)
|
||||
CODE ("\x88\x18") // mov [rax], bl
|
||||
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case RIGHT:
|
||||
// add rax, "arg" -- optimistic, no boundary checking
|
||||
if (arg > INT8_MAX)
|
||||
{ CODE ("\x48\x05") DWRD (arg) }
|
||||
else
|
||||
{ CODE ("\x48\x83\xC0") str_append_c (&program, arg); }
|
||||
break;
|
||||
case LEFT:
|
||||
// sub rax, "arg" -- optimistic, no boundary checking
|
||||
if (arg > INT8_MAX)
|
||||
{ CODE ("\x48\x2D") DWRD (arg) }
|
||||
else
|
||||
{ CODE ("\x48\x83\xE8") str_append_c (&program, arg); }
|
||||
break;
|
||||
|
||||
case EAT:
|
||||
CODE ("\x41\x88\xDC") // mov r12b, bl
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case INCACC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x44\x00\x60") // add [rax+"offset"], r12b
|
||||
str_append_c (&program, offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
CODE ("\x44\x00\xE3") // add bl, r12b
|
||||
arith[i] = 1;
|
||||
}
|
||||
break;
|
||||
case DECACC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x44\x28\x60") // sub [rax+"offset"], r12b
|
||||
str_append_c (&program, offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
CODE ("\x44\x28\xE3") // sub bl, r12b
|
||||
arith[i] = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case INC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x80\x40") // add byte [rax+"offset"], "arg"
|
||||
str_append_c (&program, offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
arith[i] = 1;
|
||||
CODE ("\x80\xC3") // add bl, "arg"
|
||||
}
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
case DEC:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\x80\x68") // sub byte [rax+"offset"], "arg"
|
||||
str_append_c (&program, offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
arith[i] = 1;
|
||||
CODE ("\x80\xEB") // sub bl, "arg"
|
||||
}
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
case SET:
|
||||
if (offset)
|
||||
{
|
||||
CODE ("\xC6\x40") // mov byte [rax+"offset"], "arg"
|
||||
str_append_c (&program, offset);
|
||||
}
|
||||
else
|
||||
CODE ("\xB3") // mov bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
|
||||
case OUT:
|
||||
CODE ("\x50\x53") // push rax, push rbx
|
||||
CODE ("\x48\x0F\xB6\xFB") // movzx rdi, bl
|
||||
CODE ("\x48\xBE") WORD (stdout) // mov rsi, "stdout"
|
||||
CODE ("\x48\xB8") WORD (fputc) // mov rax, "fputc"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x5B\x58") // pop rbx, pop rax
|
||||
break;
|
||||
case IN:
|
||||
CODE ("\x50") // push rax
|
||||
CODE ("\x48\xB8") WORD (cin) // mov rax, "cin"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x88\xC3") // mov bl, al
|
||||
CODE ("\x58") // pop rax
|
||||
break;
|
||||
|
||||
case BEGIN:
|
||||
// Don't test the register when the flag has been set already;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!i || !arith[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x84\x00\x00\x00\x00") // jz "offsets[i]"
|
||||
break;
|
||||
case END:
|
||||
// We know that the cell is zero, make this an "if", not a "loop";
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!arg)
|
||||
break;
|
||||
|
||||
if (!i || !arith[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x85\x00\x00\x00\x00") // jnz "offsets[i]"
|
||||
break;
|
||||
}
|
||||
|
||||
// No sense in reading it out when we overwrite it immediately;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (parsed[i].cmd == LEFT || parsed[i].cmd == RIGHT)
|
||||
if (i + 1 >= parsed_len
|
||||
|| parsed[i + 1].cmd != SET
|
||||
|| parsed[i + 1].offset != 0)
|
||||
CODE ("\x8A\x18") // mov bl, [rax]
|
||||
}
|
||||
// When there is a loop at the end we need to be able to jump past it
|
||||
offsets[parsed_len] = program.len;
|
||||
str_append_c (&program, '\xC3'); // ret
|
||||
|
||||
// Now that we know where each instruction is, fill in relative jumps;
|
||||
// this must accurately reflect code generators for BEGIN and END
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
if ((parsed[i].cmd != BEGIN && parsed[i].cmd != END)
|
||||
|| !parsed[i].arg)
|
||||
continue;
|
||||
|
||||
size_t fixup = offsets[i] + 2;
|
||||
if (!i || !arith[i - 1])
|
||||
fixup += 2;
|
||||
|
||||
*(int32_t *)(program.str + fixup) =
|
||||
((intptr_t)(offsets[parsed[i].arg]) - (intptr_t)(fixup + 4));
|
||||
}
|
||||
free (offsets);
|
||||
free (arith);
|
||||
|
||||
#ifdef DEBUG
|
||||
FILE *bin = fopen ("bf-jit.bin", "w");
|
||||
fwrite (program.str, program.len, 1, bin);
|
||||
fclose (bin);
|
||||
#endif
|
||||
|
||||
// - - Runtime - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Some systems may have W^X
|
||||
void *executable = mmap (NULL, program.len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (!executable)
|
||||
exit_fatal ("mmap: %s\n", strerror (errno));
|
||||
|
||||
memcpy (executable, program.str, program.len);
|
||||
if (mprotect (executable, program.len, PROT_READ | PROT_EXEC))
|
||||
exit_fatal ("mprotect: %s\n", strerror (errno));
|
||||
|
||||
// We create crash zones on both ends of the tape for some minimum safety
|
||||
long pagesz = sysconf (_SC_PAGESIZE);
|
||||
assert (pagesz > 0);
|
||||
|
||||
const size_t tape_len = (1 << 20) + 2 * pagesz;
|
||||
char *tape = mmap (NULL, tape_len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (!tape)
|
||||
exit_fatal ("mmap: %s\n", strerror (errno));
|
||||
|
||||
memset (tape, 0, tape_len);
|
||||
if (mprotect (tape, pagesz, PROT_NONE)
|
||||
|| mprotect (tape + tape_len - pagesz, pagesz, PROT_NONE))
|
||||
exit_fatal ("mprotect: %s\n", strerror (errno));
|
||||
|
||||
((void (*) (char *)) executable)(tape + pagesz);
|
||||
return 0;
|
||||
}
|
495
interpreters/bf-jit-unsafe.c
Normal file
495
interpreters/bf-jit-unsafe.c
Normal file
@ -0,0 +1,495 @@
|
||||
// This is an exercise in futility more than anything else
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#if (defined __x86_64__ || defined __amd64__) && defined __unix__
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#else
|
||||
#error Platform not supported
|
||||
#endif
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xcalloc (size_t m, size_t n)
|
||||
{
|
||||
void *p = calloc (m, n);
|
||||
if (!p)
|
||||
exit_fatal ("calloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->len = 0;
|
||||
self->str = xcalloc (1, (self->alloc = 16));
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Application -------------------------------------------------------------
|
||||
|
||||
enum command { RIGHT, LEFT, INC, DEC, SET, IN, OUT, BEGIN, END,
|
||||
EAT, INCACC, DECACC };
|
||||
bool grouped[] = { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 };
|
||||
struct instruction { enum command cmd; size_t arg; };
|
||||
#define INSTRUCTION(c, a) (struct instruction) { (c), (a) }
|
||||
|
||||
// - - Callbacks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
FILE *input; ///< User input
|
||||
|
||||
static int
|
||||
cin (void)
|
||||
{
|
||||
int c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
return c;
|
||||
}
|
||||
|
||||
// - - Main - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
#ifdef DEBUG
|
||||
static void
|
||||
debug_dump (const char *filename, struct instruction *in, size_t len)
|
||||
{
|
||||
FILE *fp = fopen (filename, "w");
|
||||
long indent = 0;
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (in[i].cmd == END)
|
||||
indent--;
|
||||
for (long k = 0; k < indent; k++)
|
||||
fprintf (fp, " ");
|
||||
|
||||
switch (in[i].cmd)
|
||||
{
|
||||
case RIGHT: fprintf (fp, "RIGHT %zu\n", in[i].arg); break;
|
||||
case LEFT: fprintf (fp, "LEFT %zu\n", in[i].arg); break;
|
||||
case INC: fprintf (fp, "INC %zu\n", in[i].arg); break;
|
||||
case DEC: fprintf (fp, "DEC %zu\n", in[i].arg); break;
|
||||
case OUT: fprintf (fp, "OUT %zu\n", in[i].arg); break;
|
||||
case IN: fprintf (fp, "IN %zu\n", in[i].arg); break;
|
||||
case BEGIN: fprintf (fp, "BEGIN %zu\n", in[i].arg); break;
|
||||
case END: fprintf (fp, "END %zu\n", in[i].arg); break;
|
||||
case SET: fprintf (fp, "SET %zu\n", in[i].arg); break;
|
||||
case EAT: fprintf (fp, "EAT %zu\n", in[i].arg); break;
|
||||
case INCACC: fprintf (fp, "INCACC %zu\n", in[i].arg); break;
|
||||
case DECACC: fprintf (fp, "DECACC %zu\n", in[i].arg); break;
|
||||
}
|
||||
if (in[i].cmd == BEGIN)
|
||||
indent++;
|
||||
}
|
||||
fclose (fp);
|
||||
}
|
||||
#else
|
||||
#define debug_dump(...)
|
||||
#endif
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
struct str program;
|
||||
str_init (&program);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
if (!(input = fopen ("/dev/tty", "rb")))
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
// - - Decode and group - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct instruction *parsed = xcalloc (sizeof *parsed, program.len);
|
||||
size_t parsed_len = 0;
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
enum command cmd;
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '>': cmd = RIGHT; break;
|
||||
case '<': cmd = LEFT; break;
|
||||
case '+': cmd = INC; break;
|
||||
case '-': cmd = DEC; break;
|
||||
case '.': cmd = OUT; break;
|
||||
case ',': cmd = IN; break;
|
||||
case '[': cmd = BEGIN; break;
|
||||
case ']': cmd = END; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
// The most basic optimization is to group identical commands together
|
||||
if (!parsed_len || !grouped[cmd] || parsed[parsed_len - 1].cmd != cmd)
|
||||
parsed_len++;
|
||||
|
||||
parsed[parsed_len - 1].cmd = cmd;
|
||||
parsed[parsed_len - 1].arg++;
|
||||
}
|
||||
|
||||
// - - Optimization passes - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
debug_dump ("bf-no-opt.txt", parsed, parsed_len);
|
||||
|
||||
size_t in = 0, out = 0;
|
||||
for (; in < parsed_len; in++, out++)
|
||||
{
|
||||
// This shows up in mandelbrot.bf a lot but actually helps hanoi.bf
|
||||
if (in + 5 < parsed_len
|
||||
&& parsed[in].cmd == BEGIN && parsed[in + 5].cmd == END
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
|
||||
&& parsed[in + 2].cmd == LEFT && parsed[in + 4].cmd == RIGHT
|
||||
&& parsed[in + 2].arg == parsed[in + 4].arg
|
||||
|
||||
&& (parsed[in + 3].cmd == INC || parsed[in + 3].cmd == DEC)
|
||||
&& parsed[in + 3].arg == 1)
|
||||
{
|
||||
// This mustn't make the move when the cell is zero already
|
||||
parsed[out] = parsed[in];
|
||||
parsed[out + 1] = INSTRUCTION (EAT, 0);
|
||||
parsed[out + 2] = parsed[in + 2];
|
||||
parsed[out + 3] = INSTRUCTION
|
||||
(parsed[in + 3].cmd == INC ? INCACC : DECACC, 0);
|
||||
parsed[out + 4] = parsed[in + 4];
|
||||
// This disables the looping further in the code;
|
||||
// this doesn't have much of an effect in practice
|
||||
parsed[out + 5] = INSTRUCTION (END, 0);
|
||||
in += 5;
|
||||
out += 5;
|
||||
}
|
||||
// The simpler case that cannot crash and thus can avoid the loop
|
||||
else if (in + 5 < parsed_len
|
||||
&& parsed[in].cmd == BEGIN && parsed[in + 5].cmd == END
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
|
||||
&& parsed[in + 2].cmd == RIGHT && parsed[in + 4].cmd == LEFT
|
||||
&& parsed[in + 2].arg == parsed[in + 4].arg
|
||||
|
||||
&& (parsed[in + 3].cmd == INC || parsed[in + 3].cmd == DEC)
|
||||
&& parsed[in + 3].arg == 1)
|
||||
{
|
||||
parsed[out] = INSTRUCTION (EAT, 0);
|
||||
parsed[out + 1] = parsed[in + 2];
|
||||
parsed[out + 2] = INSTRUCTION
|
||||
(parsed[in + 3].cmd == INC ? INCACC : DECACC, 0);
|
||||
parsed[out + 3] = parsed[in + 4];
|
||||
in += 5;
|
||||
out += 3;
|
||||
}
|
||||
else if (in + 2 < parsed_len
|
||||
&& parsed[in ].cmd == BEGIN
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
&& parsed[in + 2].cmd == END)
|
||||
{
|
||||
parsed[out] = INSTRUCTION (SET, 0);
|
||||
in += 2;
|
||||
}
|
||||
else if (out && parsed[out - 1].cmd == SET && parsed[in].cmd == INC)
|
||||
parsed[--out].arg += parsed[in].arg;
|
||||
else if (out != in)
|
||||
parsed[out] = parsed[in];
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
for (in = 0, out = 0; in < parsed_len; in++, out++)
|
||||
{
|
||||
ssize_t dir = 0;
|
||||
if (parsed[in].cmd == RIGHT)
|
||||
dir = parsed[in].arg;
|
||||
else if (parsed[in].cmd == LEFT)
|
||||
dir = -(ssize_t) parsed[in].arg;
|
||||
else
|
||||
{
|
||||
parsed[out] = parsed[in];
|
||||
continue;
|
||||
}
|
||||
|
||||
for (; in + 1 < parsed_len; in++)
|
||||
{
|
||||
if (parsed[in + 1].cmd == RIGHT)
|
||||
dir += parsed[in + 1].arg;
|
||||
else if (parsed[in + 1].cmd == LEFT)
|
||||
dir -= (ssize_t) parsed[in + 1].arg;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dir)
|
||||
out--;
|
||||
else if (dir > 0)
|
||||
parsed[out] = INSTRUCTION (RIGHT, dir);
|
||||
else
|
||||
parsed[out] = INSTRUCTION (LEFT, -dir);
|
||||
}
|
||||
parsed_len = out;
|
||||
|
||||
debug_dump ("bf-optimized.txt", parsed, parsed_len);
|
||||
|
||||
// - - Loop pairing - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t nesting = 0;
|
||||
size_t *stack = xcalloc (sizeof *stack, parsed_len);
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case BEGIN:
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case END:
|
||||
assert (nesting > 0);
|
||||
|
||||
--nesting;
|
||||
parsed[stack[nesting]].arg = i + 1;
|
||||
|
||||
// Looping can be disabled by optimizations
|
||||
if (parsed[i].arg)
|
||||
parsed[i].arg = stack[nesting] + 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
free (stack);
|
||||
assert (nesting == 0);
|
||||
|
||||
debug_dump ("bf-final.txt", parsed, parsed_len);
|
||||
|
||||
// - - JIT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Functions preserve the registers rbx, rsp, rbp, r12, r13, r14, and r15;
|
||||
// while rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 are scratch registers.
|
||||
|
||||
str_init (&program);
|
||||
size_t *offsets = xcalloc (sizeof *offsets, parsed_len + 1);
|
||||
uint8_t *arith = xcalloc (sizeof *arith, parsed_len);
|
||||
|
||||
#define CODE(x) { char t[] = x; str_append_data (&program, t, sizeof t - 1); }
|
||||
#define WORD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 8); }
|
||||
#define DWRD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 4); }
|
||||
|
||||
CODE ("\x48\x89\xF8") // mov rax, rdi
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
offsets[i] = program.len;
|
||||
|
||||
size_t arg = parsed[i].arg;
|
||||
assert (arg <= UINT32_MAX);
|
||||
|
||||
// Don't save what we've just loaded
|
||||
if (parsed[i].cmd == LEFT || parsed[i].cmd == RIGHT)
|
||||
if (i < 2 || i + 1 >= parsed_len
|
||||
|| (parsed[i - 2].cmd != LEFT && parsed[i - 2].cmd != RIGHT)
|
||||
|| parsed[i - 1].cmd != BEGIN
|
||||
|| parsed[i + 1].cmd != END)
|
||||
CODE ("\x88\x18") // mov [rax], bl
|
||||
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case RIGHT:
|
||||
// add rax, "arg" -- optimistic, no boundary checking
|
||||
if (arg > INT8_MAX)
|
||||
{ CODE ("\x48\x05") DWRD (arg) }
|
||||
else
|
||||
{ CODE ("\x48\x83\xC0") str_append_c (&program, arg); }
|
||||
break;
|
||||
case LEFT:
|
||||
// sub rax, "arg" -- optimistic, no boundary checking
|
||||
if (arg > INT8_MAX)
|
||||
{ CODE ("\x48\x2D") DWRD (arg) }
|
||||
else
|
||||
{ CODE ("\x48\x83\xE8") str_append_c (&program, arg); }
|
||||
break;
|
||||
|
||||
case EAT:
|
||||
CODE ("\x41\x88\xDC") // mov r12b, bl
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case INCACC:
|
||||
CODE ("\x44\x00\xE3") // add bl, r12b
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case DECACC:
|
||||
CODE ("\x44\x28\xE3") // sub bl, r12b
|
||||
arith[i] = 1;
|
||||
break;
|
||||
|
||||
case INC:
|
||||
CODE ("\x80\xC3") // add bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case DEC:
|
||||
CODE ("\x80\xEB") // sub bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
arith[i] = 1;
|
||||
break;
|
||||
case SET:
|
||||
CODE ("\xB3") // mov bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
|
||||
case OUT:
|
||||
CODE ("\x50\x53") // push rax, push rbx
|
||||
CODE ("\x48\x0F\xB6\xFB") // movzx rdi, bl
|
||||
CODE ("\x48\xBE") WORD (stdout) // mov rsi, "stdout"
|
||||
CODE ("\x48\xB8") WORD (fputc) // mov rax, "fputc"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x5B\x58") // pop rbx, pop rax
|
||||
break;
|
||||
case IN:
|
||||
CODE ("\x50") // push rax
|
||||
CODE ("\x48\xB8") WORD (cin) // mov rax, "cin"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x88\xC3") // mov bl, al
|
||||
CODE ("\x58") // pop rax
|
||||
break;
|
||||
|
||||
case BEGIN:
|
||||
// Don't test the register when the flag has been set already;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!i || !arith[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x84\x00\x00\x00\x00") // jz "offsets[i]"
|
||||
break;
|
||||
case END:
|
||||
// We know that the cell is zero, make this an "if", not a "loop";
|
||||
// this doesn't have much of an effect in practice
|
||||
if (!arg)
|
||||
break;
|
||||
|
||||
if (!i || !arith[i - 1])
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x85\x00\x00\x00\x00") // jnz "offsets[i]"
|
||||
break;
|
||||
}
|
||||
|
||||
// No sense in reading it out when we overwrite it immediately;
|
||||
// this doesn't have much of an effect in practice
|
||||
if (parsed[i].cmd == LEFT || parsed[i].cmd == RIGHT)
|
||||
if (i + 1 >= parsed_len
|
||||
|| parsed[i + 1].cmd != SET)
|
||||
CODE ("\x8A\x18") // mov bl, [rax]
|
||||
}
|
||||
// When there is a loop at the end we need to be able to jump past it
|
||||
offsets[parsed_len] = program.len;
|
||||
str_append_c (&program, '\xC3'); // ret
|
||||
|
||||
// Now that we know where each instruction is, fill in relative jumps;
|
||||
// this must accurately reflect code generators for BEGIN and END
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
if ((parsed[i].cmd != BEGIN && parsed[i].cmd != END)
|
||||
|| !parsed[i].arg)
|
||||
continue;
|
||||
|
||||
size_t fixup = offsets[i] + 2;
|
||||
if (!i || !arith[i - 1])
|
||||
fixup += 2;
|
||||
|
||||
*(int32_t *)(program.str + fixup) =
|
||||
((intptr_t)(offsets[parsed[i].arg]) - (intptr_t)(fixup + 4));
|
||||
}
|
||||
free (offsets);
|
||||
free (arith);
|
||||
|
||||
#ifdef DEBUG
|
||||
FILE *bin = fopen ("bf-jit.bin", "w");
|
||||
fwrite (program.str, program.len, 1, bin);
|
||||
fclose (bin);
|
||||
#endif
|
||||
|
||||
// - - Runtime - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Some systems may have W^X
|
||||
void *executable = mmap (NULL, program.len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (!executable)
|
||||
exit_fatal ("mmap: %s\n", strerror (errno));
|
||||
|
||||
memcpy (executable, program.str, program.len);
|
||||
if (mprotect (executable, program.len, PROT_READ | PROT_EXEC))
|
||||
exit_fatal ("mprotect: %s\n", strerror (errno));
|
||||
|
||||
// We create crash zones on both ends of the tape for some minimum safety
|
||||
long pagesz = sysconf (_SC_PAGESIZE);
|
||||
assert (pagesz > 0);
|
||||
|
||||
const size_t tape_len = (1 << 20) + 2 * pagesz;
|
||||
char *tape = mmap (NULL, tape_len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (!tape)
|
||||
exit_fatal ("mmap: %s\n", strerror (errno));
|
||||
|
||||
memset (tape, 0, tape_len);
|
||||
if (mprotect (tape, pagesz, PROT_NONE)
|
||||
|| mprotect (tape + tape_len - pagesz, pagesz, PROT_NONE))
|
||||
exit_fatal ("mprotect: %s\n", strerror (errno));
|
||||
|
||||
((void (*) (char *)) executable)(tape + pagesz);
|
||||
return 0;
|
||||
}
|
327
interpreters/bf-jit.c
Normal file
327
interpreters/bf-jit.c
Normal file
@ -0,0 +1,327 @@
|
||||
// This is an exercise in futility more than anything else
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#if (defined __x86_64__ || defined __amd64__) && defined __unix__
|
||||
#include <sys/mman.h>
|
||||
#else
|
||||
#error Platform not supported
|
||||
#endif
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xcalloc (size_t m, size_t n)
|
||||
{
|
||||
void *p = calloc (m, n);
|
||||
if (!p)
|
||||
exit_fatal ("calloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->len = 0;
|
||||
self->str = xcalloc (1, (self->alloc = 16));
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Application -------------------------------------------------------------
|
||||
|
||||
struct str data; ///< Data tape
|
||||
volatile size_t dataptr; ///< Current location on the tape
|
||||
FILE *input; ///< User input
|
||||
|
||||
enum command { RIGHT, LEFT, INC, DEC, SET, IN, OUT, BEGIN, END };
|
||||
bool grouped[] = { 1, 1, 1, 1, 1, 0, 0, 0, 0 };
|
||||
struct instruction { enum command cmd; size_t arg; };
|
||||
|
||||
// - - Callbacks - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Some things I just really don't want to write in assembly even though it
|
||||
// is effectively a big performance hit, eliminating the advantage of JIT
|
||||
|
||||
static void
|
||||
right (size_t arg)
|
||||
{
|
||||
assert (SIZE_MAX - dataptr > arg);
|
||||
dataptr += arg;
|
||||
|
||||
while (dataptr >= data.len)
|
||||
str_append_c (&data, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
left (size_t arg)
|
||||
{
|
||||
assert (dataptr >= arg);
|
||||
dataptr -= arg;
|
||||
}
|
||||
|
||||
static int
|
||||
cin (void)
|
||||
{
|
||||
int c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
return c;
|
||||
}
|
||||
|
||||
// - - Main - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
struct str program;
|
||||
str_init (&program);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
if (!(input = fopen ("/dev/tty", "rb")))
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
// - - Decode and group - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct instruction *parsed = xcalloc (sizeof *parsed, program.len);
|
||||
size_t parsed_len = 0;
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
enum command cmd;
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '>': cmd = RIGHT; break;
|
||||
case '<': cmd = LEFT; break;
|
||||
case '+': cmd = INC; break;
|
||||
case '-': cmd = DEC; break;
|
||||
case '.': cmd = OUT; break;
|
||||
case ',': cmd = IN; break;
|
||||
case '[': cmd = BEGIN; break;
|
||||
case ']': cmd = END; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
if (!parsed_len || !grouped[cmd] || parsed[parsed_len - 1].cmd != cmd)
|
||||
parsed_len++;
|
||||
|
||||
parsed[parsed_len - 1].cmd = cmd;
|
||||
parsed[parsed_len - 1].arg++;
|
||||
}
|
||||
|
||||
// - - Simple optimization pass - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t in = 0, out = 0;
|
||||
for (; in < parsed_len; in++, out++)
|
||||
{
|
||||
if (in + 2 < parsed_len
|
||||
&& parsed[in ].cmd == BEGIN
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
&& parsed[in + 2].cmd == END)
|
||||
{
|
||||
parsed[out].cmd = SET;
|
||||
parsed[out].arg = 0;
|
||||
in += 2;
|
||||
}
|
||||
else if (out && parsed[out - 1].cmd == SET && parsed[in].cmd == INC)
|
||||
parsed[--out].arg += parsed[in].arg;
|
||||
else if (out != in)
|
||||
parsed[out] = parsed[in];
|
||||
}
|
||||
|
||||
parsed_len = out;
|
||||
|
||||
// - - Loop pairing - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t nesting = 0;
|
||||
size_t *stack = xcalloc (sizeof *stack, parsed_len);
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case BEGIN:
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case END:
|
||||
assert (nesting > 0);
|
||||
|
||||
--nesting;
|
||||
parsed[stack[nesting]].arg = i + 1;
|
||||
parsed[i].arg = stack[nesting] + 1;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
free (stack);
|
||||
assert (nesting == 0);
|
||||
|
||||
// - - JIT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Functions preserve the registers rbx, rsp, rbp, r12, r13, r14, and r15;
|
||||
// while rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 are scratch registers.
|
||||
|
||||
str_init (&program);
|
||||
size_t *offsets = xcalloc (sizeof *offsets, parsed_len + 1);
|
||||
|
||||
#define CODE(x) { char t[] = x; str_append_data (&program, t, sizeof t - 1); }
|
||||
#define WORD(x) { size_t t = (size_t)(x); str_append_data (&program, &t, 8); }
|
||||
|
||||
CODE ("\x49\xBD") WORD (&dataptr) // mov r13, qword "&dataptr"
|
||||
CODE ("\x49\xBF") WORD (&data.str) // mov r15, qword "&data.str"
|
||||
CODE ("\x4D\x8B\x37") // mov r14, qword [r15]
|
||||
CODE ("\x30\xDB") // xor bl, bl
|
||||
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
offsets[i] = program.len;
|
||||
|
||||
size_t arg = parsed[i].arg;
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case RIGHT:
|
||||
CODE ("\x41\x88\x1E") // mov [r14], bl
|
||||
CODE ("\x48\xBF") WORD (arg) // mov rdi, "arg"
|
||||
CODE ("\x48\xB8") WORD (right) // mov rax, "right"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
|
||||
// The data could get reallocated, so reload the address
|
||||
CODE ("\x4D\x8B\x37") // mov r14, qword [r15]
|
||||
CODE ("\x4D\x03\x75\x00") // add r14, [r13]
|
||||
CODE ("\x41\x8A\x1E") // mov bl, [r14]
|
||||
break;
|
||||
case LEFT:
|
||||
CODE ("\x41\x88\x1E") // mov [r14], bl
|
||||
CODE ("\x48\xBF") WORD (arg) // mov rdi, "arg"
|
||||
CODE ("\x49\x29\xFE") // sub r14, rdi -- optimistic
|
||||
CODE ("\x48\xB8") WORD (left) // mov rax, "left"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x41\x8A\x1E") // mov bl, [r14]
|
||||
break;
|
||||
|
||||
case INC:
|
||||
CODE ("\x80\xC3") // add bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
case DEC:
|
||||
CODE ("\x80\xEB") // sub bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
case SET:
|
||||
CODE ("\xB3") // mov bl, "arg"
|
||||
str_append_c (&program, arg);
|
||||
break;
|
||||
|
||||
case OUT:
|
||||
CODE ("\x48\x0F\xB6\xFB") // movzx rdi, bl
|
||||
CODE ("\x48\xBE") WORD (stdout) // mov rsi, "stdout"
|
||||
CODE ("\x48\xB8") WORD (fputc) // mov rax, "fputc"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
break;
|
||||
case IN:
|
||||
CODE ("\x48\xB8") WORD (cin) // mov rax, "cin"
|
||||
CODE ("\xFF\xD0") // call rax
|
||||
CODE ("\x88\xC3") // mov bl, al
|
||||
break;
|
||||
|
||||
case BEGIN:
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x84\x00\x00\x00\x00") // jz "offsets[i]"
|
||||
break;
|
||||
case END:
|
||||
CODE ("\x84\xDB") // test bl, bl
|
||||
CODE ("\x0F\x85\x00\x00\x00\x00") // jnz "offsets[i]"
|
||||
break;
|
||||
}
|
||||
}
|
||||
// When there is a loop at the end we need to be able to jump past it
|
||||
offsets[parsed_len] = program.len;
|
||||
str_append_c (&program, '\xC3'); // ret
|
||||
|
||||
// Now that we know where each instruction is, fill in relative jumps
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
if (parsed[i].cmd != BEGIN && parsed[i].cmd != END)
|
||||
continue;
|
||||
size_t fixup = offsets[i] + 4;
|
||||
*(int32_t *)(program.str + fixup) =
|
||||
((intptr_t)(offsets[parsed[i].arg]) - (intptr_t)(fixup + 4));
|
||||
}
|
||||
free (offsets);
|
||||
|
||||
// - - Runtime - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
// Some systems may have W^X
|
||||
void *executable = mmap (NULL, program.len, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (!executable)
|
||||
exit_fatal ("mmap: %s\n", strerror (errno));
|
||||
|
||||
memcpy (executable, program.str, program.len);
|
||||
if (mprotect (executable, program.len, PROT_READ | PROT_EXEC))
|
||||
exit_fatal ("mprotect: %s\n", strerror (errno));
|
||||
|
||||
str_init (&data);
|
||||
str_append_c (&data, 0);
|
||||
((void (*) (void)) executable)();
|
||||
return 0;
|
||||
}
|
213
interpreters/bf-optimizing.c
Normal file
213
interpreters/bf-optimizing.c
Normal file
@ -0,0 +1,213 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xcalloc (size_t m, size_t n)
|
||||
{
|
||||
void *p = calloc (m, n);
|
||||
if (!p)
|
||||
exit_fatal ("calloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->len = 0;
|
||||
self->str = xcalloc (1, (self->alloc = 16));
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->str[self->len += n] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Main --------------------------------------------------------------------
|
||||
|
||||
struct str program; ///< Raw program
|
||||
struct str data; ///< Data tape
|
||||
|
||||
enum command { RIGHT, LEFT, INC, DEC, SET, IN, OUT, BEGIN, END };
|
||||
bool grouped[] = { 1, 1, 1, 1, 1, 0, 0, 0, 0 };
|
||||
struct instruction { enum command cmd; size_t arg; };
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
(void) argc; str_init (&program);
|
||||
(void) argv; str_init (&data);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
|
||||
FILE *input = fopen ("/dev/tty", "rb");
|
||||
if (!input)
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
// - - Decode and group - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
struct instruction *parsed = xcalloc (sizeof *parsed, program.len);
|
||||
size_t parsed_len = 0;
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
enum command cmd;
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '>': cmd = RIGHT; break;
|
||||
case '<': cmd = LEFT; break;
|
||||
case '+': cmd = INC; break;
|
||||
case '-': cmd = DEC; break;
|
||||
case '.': cmd = OUT; break;
|
||||
case ',': cmd = IN; break;
|
||||
case '[': cmd = BEGIN; break;
|
||||
case ']': cmd = END; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
if (!parsed_len || !grouped[cmd] || parsed[parsed_len - 1].cmd != cmd)
|
||||
parsed_len++;
|
||||
|
||||
parsed[parsed_len - 1].cmd = cmd;
|
||||
parsed[parsed_len - 1].arg++;
|
||||
}
|
||||
|
||||
// - - Simple optimization pass - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t in = 0, out = 0;
|
||||
for (; in < parsed_len; in++, out++)
|
||||
{
|
||||
if (in + 2 < parsed_len
|
||||
&& parsed[in ].cmd == BEGIN
|
||||
&& parsed[in + 1].cmd == DEC && parsed[in + 1].arg == 1
|
||||
&& parsed[in + 2].cmd == END)
|
||||
{
|
||||
parsed[out].cmd = SET;
|
||||
parsed[out].arg = 0;
|
||||
in += 2;
|
||||
}
|
||||
else if (out && parsed[out - 1].cmd == SET && parsed[in].cmd == INC)
|
||||
parsed[--out].arg += parsed[in].arg;
|
||||
else if (out != in)
|
||||
parsed[out] = parsed[in];
|
||||
}
|
||||
|
||||
parsed_len = out;
|
||||
|
||||
// - - Loop pairing - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t nesting = 0;
|
||||
size_t *stack = xcalloc (sizeof *stack, parsed_len);
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case BEGIN:
|
||||
stack[nesting++] = i;
|
||||
break;
|
||||
case END:
|
||||
assert (nesting > 0);
|
||||
|
||||
--nesting;
|
||||
parsed[stack[nesting]].arg = i;
|
||||
parsed[i].arg = stack[nesting];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert (nesting == 0);
|
||||
|
||||
// - - Runtime - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
size_t dataptr = 0;
|
||||
str_append_c (&data, 0);
|
||||
|
||||
for (size_t i = 0; i < parsed_len; i++)
|
||||
{
|
||||
size_t arg = parsed[i].arg;
|
||||
switch (parsed[i].cmd)
|
||||
{
|
||||
case RIGHT:
|
||||
assert (SIZE_MAX - dataptr > arg);
|
||||
dataptr += arg;
|
||||
|
||||
while (dataptr >= data.len)
|
||||
str_append_c (&data, 0);
|
||||
break;
|
||||
case LEFT:
|
||||
assert (dataptr >= arg);
|
||||
dataptr -= arg;
|
||||
break;
|
||||
|
||||
case INC: data.str[dataptr] += arg; break;
|
||||
case DEC: data.str[dataptr] -= arg; break;
|
||||
case SET: data.str[dataptr] = arg; break;
|
||||
|
||||
case OUT:
|
||||
fputc (data.str[dataptr], stdout);
|
||||
break;
|
||||
case IN:
|
||||
data.str[dataptr] = c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
break;
|
||||
|
||||
case BEGIN: if (!data.str[dataptr]) i = arg; break;
|
||||
case END: if ( data.str[dataptr]) i = arg; break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
160
interpreters/bf.c
Normal file
160
interpreters/bf.c
Normal file
@ -0,0 +1,160 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define exit_fatal(...) \
|
||||
do { \
|
||||
fprintf (stderr, "fatal: " __VA_ARGS__); \
|
||||
exit (EXIT_FAILURE); \
|
||||
} while (0)
|
||||
|
||||
// --- Safe memory management --------------------------------------------------
|
||||
|
||||
static void *
|
||||
xmalloc (size_t n)
|
||||
{
|
||||
void *p = malloc (n);
|
||||
if (!p)
|
||||
exit_fatal ("malloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
static void *
|
||||
xrealloc (void *o, size_t n)
|
||||
{
|
||||
void *p = realloc (o, n);
|
||||
if (!p && n)
|
||||
exit_fatal ("realloc: %s\n", strerror (errno));
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- Dynamically allocated strings -------------------------------------------
|
||||
|
||||
struct str
|
||||
{
|
||||
char *str; ///< String data, null terminated
|
||||
size_t alloc; ///< How many bytes are allocated
|
||||
size_t len; ///< How long the string actually is
|
||||
};
|
||||
|
||||
static void
|
||||
str_init (struct str *self)
|
||||
{
|
||||
self->alloc = 16;
|
||||
self->len = 0;
|
||||
self->str = strcpy (xmalloc (self->alloc), "");
|
||||
}
|
||||
|
||||
static void
|
||||
str_ensure_space (struct str *self, size_t n)
|
||||
{
|
||||
// We allocate at least one more byte for the terminating null character
|
||||
size_t new_alloc = self->alloc;
|
||||
while (new_alloc <= self->len + n)
|
||||
new_alloc <<= 1;
|
||||
if (new_alloc != self->alloc)
|
||||
self->str = xrealloc (self->str, (self->alloc = new_alloc));
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_data (struct str *self, const void *data, size_t n)
|
||||
{
|
||||
str_ensure_space (self, n);
|
||||
memcpy (self->str + self->len, data, n);
|
||||
self->len += n;
|
||||
self->str[self->len] = '\0';
|
||||
}
|
||||
|
||||
static void
|
||||
str_append_c (struct str *self, char c)
|
||||
{
|
||||
str_append_data (self, &c, 1);
|
||||
}
|
||||
|
||||
// --- Main --------------------------------------------------------------------
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
struct str program; str_init (&program);
|
||||
struct str data; str_init (&data);
|
||||
|
||||
int c;
|
||||
while ((c = fgetc (stdin)) != EOF)
|
||||
str_append_c (&program, c);
|
||||
if (ferror (stdin))
|
||||
exit_fatal ("can't read program\n");
|
||||
|
||||
FILE *input = fopen ("/dev/tty", "rb");
|
||||
if (!input)
|
||||
exit_fatal ("can't open terminal for reading\n");
|
||||
|
||||
size_t dataptr = 0;
|
||||
str_append_c (&data, 0);
|
||||
|
||||
for (size_t i = 0; i < program.len; i++)
|
||||
{
|
||||
switch (program.str[i])
|
||||
{
|
||||
long pairs;
|
||||
case '>':
|
||||
assert (dataptr != SIZE_MAX);
|
||||
dataptr++;
|
||||
if (dataptr == data.len)
|
||||
str_append_c (&data, 0);
|
||||
break;
|
||||
case '<':
|
||||
assert (dataptr != 0);
|
||||
dataptr--;
|
||||
break;
|
||||
|
||||
case '+': data.str[dataptr]++; break;
|
||||
case '-': data.str[dataptr]--; break;
|
||||
|
||||
case '.':
|
||||
fputc (data.str[dataptr], stdout);
|
||||
break;
|
||||
case ',':
|
||||
data.str[dataptr] = c = fgetc (input);
|
||||
assert (c != EOF);
|
||||
break;
|
||||
|
||||
case '[':
|
||||
if (data.str[dataptr]) break;
|
||||
|
||||
for (pairs = 0; i < program.len; i++)
|
||||
{
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '[': pairs++; break;
|
||||
case ']': pairs--; break;
|
||||
}
|
||||
if (!pairs)
|
||||
break;
|
||||
}
|
||||
assert (!pairs);
|
||||
break;
|
||||
case ']':
|
||||
if (!data.str[dataptr]) break;
|
||||
|
||||
for (pairs = 0; i != SIZE_MAX; i--)
|
||||
{
|
||||
switch (program.str[i])
|
||||
{
|
||||
case '[': pairs--; break;
|
||||
case ']': pairs++; break;
|
||||
}
|
||||
if (!pairs)
|
||||
break;
|
||||
}
|
||||
assert (!pairs);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user