[i-g-t RFC 3/3] Preliminary fuzzing/test minimisation

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



My goal with enabling kcov is to use the coverage to guide test creation,
and ideally automate as much as the test generation as possible. The
argument is that since we only have x minutes to run BAT, we want to
choose wisely what paths we test to gain the most coverage.

Code coverage (in this case edges between basic blocks) is a very
rudimentary attempt to try and detect paths through GPU state and driver
behaviour.

Further limitations: a scripting language limits what kernel operations we
can perform. (But we do need verification capabilities, the language is
meant for writing tests.) To do exhaustive ioctl fuzzing, we need
something around the lines of trinity or syzkaller. I'm tempted to just
feed binary data into the drm-fd ioctl and let afl-fuzz play. (Hmm,
something like "[ mix of integers, objects (pointers) ] cmd ioctl" would
allow arbitrary escaping from the "safe" test language.)

The scripting language used here is based on postscript (a stack based,
postfix language with a very simple grammar, it is either this or
Forth! or lisp?). For example, this reproduces the gem_sync BAT:

    (intel) driver

    % for each engine, submit + wait
    /engines get { {
	pop exch null batch
	{ { << /engine 4 index >> exec wait } interrupt
      } 10 exch timeout } 1 exch fork pop
    } bind foreach waitchildren

    % submit a batch to all engines, then wait x nCPU
    { pop null batch exch {
	{ /engines get { 3 -1 roll exch << /engine 3 -1 roll >> exec exch
      } foreach exch wait exch } interrupt } 10 exch timeout
    } bind -1 exch fork waitchildren

The choice in language is driven by human comprehension vs machine
mutatability. One key factor to remember is that our testing is for
system behaviour. Ideas?
---
 Makefile.am              |    2 +-
 configure.ac             |    1 +
 fuzz/.gitignore          |    1 +
 fuzz/Makefile.am         |   40 ++
 fuzz/Makefile.sources    |    0
 fuzz/README              |    6 +
 fuzz/file.c              |  630 +++++++++++++++++++++++++
 fuzz/gemscript.c         | 1088 +++++++++++++++++++++++++++++++++++++++++++
 fuzz/hash.c              |  398 ++++++++++++++++
 fuzz/interpreter.c       |  362 +++++++++++++++
 fuzz/objects.c           |  467 +++++++++++++++++++
 fuzz/operators.c         | 1161 ++++++++++++++++++++++++++++++++++++++++++++++
 fuzz/scanner.c           |  938 +++++++++++++++++++++++++++++++++++++
 fuzz/script.h            |  532 +++++++++++++++++++++
 fuzz/scripts/context.gem |   22 +
 fuzz/scripts/store.gem   |   16 +
 fuzz/scripts/sync.gem    |   17 +
 fuzz/scripts/write.gem   |   14 +
 fuzz/stack.c             |  119 +++++
 lib/igt_gt.c             |    3 +-
 lib/igt_gt.h             |    2 +-
 21 files changed, 5816 insertions(+), 3 deletions(-)
 create mode 100644 fuzz/.gitignore
 create mode 100644 fuzz/Makefile.am
 create mode 100644 fuzz/Makefile.sources
 create mode 100644 fuzz/README
 create mode 100644 fuzz/file.c
 create mode 100644 fuzz/gemscript.c
 create mode 100644 fuzz/hash.c
 create mode 100644 fuzz/interpreter.c
 create mode 100644 fuzz/objects.c
 create mode 100644 fuzz/operators.c
 create mode 100644 fuzz/scanner.c
 create mode 100644 fuzz/script.h
 create mode 100644 fuzz/scripts/context.gem
 create mode 100644 fuzz/scripts/store.gem
 create mode 100644 fuzz/scripts/sync.gem
 create mode 100644 fuzz/scripts/write.gem
 create mode 100644 fuzz/stack.c

diff --git a/Makefile.am b/Makefile.am
index 6016862..3a0c78d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -21,7 +21,7 @@
 
 ACLOCAL_AMFLAGS = ${ACLOCAL_FLAGS} -I m4
 
-SUBDIRS = lib man tools scripts benchmarks
+SUBDIRS = lib man tools scripts benchmarks fuzz
 
 if BUILD_TESTS
 SUBDIRS += tests
diff --git a/configure.ac b/configure.ac
index 40fc7ac..bfaaaaa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -283,6 +283,7 @@ AC_CONFIG_FILES([
 		 docs/reference/Makefile
 		 docs/reference/intel-gpu-tools/Makefile
 		 docs/reference/intel-gpu-tools/version.xml
+		 fuzz/Makefile
 		 lib/Makefile
 		 lib/tests/Makefile
 		 man/Makefile
diff --git a/fuzz/.gitignore b/fuzz/.gitignore
new file mode 100644
index 0000000..202293f
--- /dev/null
+++ b/fuzz/.gitignore
@@ -0,0 +1 @@
+gemscript
diff --git a/fuzz/Makefile.am b/fuzz/Makefile.am
new file mode 100644
index 0000000..5ce7060
--- /dev/null
+++ b/fuzz/Makefile.am
@@ -0,0 +1,40 @@
+include Makefile.sources
+
+noinst_LTLIBRARIES = libscript.la
+
+libscript_la_SOURCES = \
+	file.c \
+	hash.c \
+	interpreter.c \
+	objects.c \
+	operators.c \
+	scanner.c \
+	script.h \
+	stack.c \
+	$(NULL)
+libscript_la_LIBADD = -lm
+
+noinst_PROGRAMS = \
+	gemscript \
+	$(NULL)
+
+AM_CFLAGS = $(DRM_CFLAGS) $(CWARNFLAGS) $(DEBUG_CFLAGS)\
+	-I$(srcdir)/.. \
+	-I$(srcdir)/../lib \
+	-include "$(srcdir)/../lib/check-ndebug.h" \
+	-DIGT_SRCDIR=\""$(abs_srcdir)"\" \
+	-DIGT_DATADIR=\""$(pkgdatadir)"\" \
+	$(LIBUNWIND_CFLAGS) $(WERROR_CFLAGS) \
+	$(LIBUDEV_CFLAGS) $(CAIRO_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = -Wl,--as-needed
+
+gemscript_SOURCES = \
+	gemscript.c \
+	$(NULL)
+gemscript_LDADD = libscript.la ../lib/libintel_tools.la -lm
+
+if BUILD_TESTS
+endif
+
diff --git a/fuzz/Makefile.sources b/fuzz/Makefile.sources
new file mode 100644
index 0000000..e69de29
diff --git a/fuzz/README b/fuzz/README
new file mode 100644
index 0000000..c629e3d
--- /dev/null
+++ b/fuzz/README
@@ -0,0 +1,6 @@
+The intent here is to automate test developement by fuzzing the inputs to
+the DRM ioctls. We use a domain specific language (i.e. scripts) to generate
+a template for tests that we can feed into coverage driven fuzzers like
+american-fuzzy loop (afl-fuzzer).
+
+afl-fuzz -x ./keywords.gem -i ./scripts/ -o afl -- ./gemscript
diff --git a/fuzz/file.c b/fuzz/file.c
new file mode 100644
index 0000000..c158ea4
--- /dev/null
+++ b/fuzz/file.c
@@ -0,0 +1,630 @@
+#include <stdio.h>
+#include <limits.h> /* INT_MAX */
+#include <string.h>
+#include <zlib.h>
+
+#include "script.h"
+
+#define CHUNK_SIZE 32768
+
+#define OWN_STREAM 0x1
+
+static inline obj_t new_file(struct file *file, struct script *ctx)
+{
+	return new_heap_object(&file->base, ctx, OBJ_TYPE_FILE);
+}
+
+obj_t file_new(struct script *ctx, const char *path, const char *mode)
+{
+	struct file *file = script_slab_alloc (ctx, sizeof (struct file));
+
+	file->data = NULL;
+	file->type = STDIO;
+	file->flags = OWN_STREAM;
+	file->src = fopen (path, mode);
+	if (file->src == NULL) {
+		script_slab_free (ctx, file, sizeof (struct file));
+		longjmp(ctx->error, -ENOENT);
+	}
+
+	file->data = script_alloc(ctx, 1, CHUNK_SIZE);
+	file->bp = file->data;
+	file->rem = 0;
+
+	return new_file(file, ctx);
+}
+
+obj_t file_new_for_stream (struct script *ctx, FILE *stream)
+{
+	struct file *file = script_slab_alloc (ctx, sizeof (struct file));
+
+	file->data = NULL;
+	file->type = STDIO;
+	file->flags = 0;
+	file->src = stream;
+	if (file->src == NULL) {
+		script_slab_free (ctx, file, sizeof (struct file));
+		longjmp(ctx->error, -ENOENT);
+	}
+
+	file->data = script_alloc(ctx, 1, CHUNK_SIZE);
+	file->bp = file->data;
+	file->rem = 0;
+
+	return new_file(file, ctx);
+}
+
+obj_t file_new_for_bytes(struct script *ctx,
+			 const char *bytes, unsigned int length)
+{
+	struct file *file = script_slab_alloc (ctx, sizeof (struct file));
+
+	file->type = BYTES;
+	file->src  = (uint8_t *) bytes;
+	file->data = (uint8_t *) bytes;
+	file->bp   = (uint8_t *) bytes;
+	file->rem  = length;
+
+	return new_file(file, ctx);
+}
+
+obj_t
+file_new_from_string(struct script *ctx, struct string *src)
+{
+	struct file *file = script_slab_alloc (ctx, sizeof (struct file));
+
+	if (src->deflate) {
+		uLongf len = src->deflate;
+		obj_t tmp_obj;
+
+		tmp_obj = string_new (ctx,  NULL, src->deflate);
+		switch (src->method) {
+		case NONE:
+		default:
+			longjmp(ctx->error, -ENOMEM);
+			break;
+
+		case ZLIB:
+#if HAVE_ZLIB
+			if (uncompress ((Bytef *) tmp_obj.string->string, &len,
+					(Bytef *) src->string, src->len) != Z_OK)
+#endif
+				longjmp(ctx->error, -ENOMEM);
+			break;
+		case LZO:
+#if HAVE_LZO
+			if (lzo2a_decompress ((lzo_bytep) src->string, src->len,
+					      (lzo_bytep) tmp_obj.string->string, &len,
+					      NULL))
+#endif
+				longjmp(ctx->error, -ENOMEM);
+			break;
+		}
+
+		file->src  = tmp_obj.string;
+		file->data = tmp_obj.string->string;
+		file->rem  = tmp_obj.string->len;
+	} else {
+		file->src  = src; src->base.ref++;
+		file->data = src->string;
+		file->rem  = src->len;
+	}
+	file->type = BYTES;
+	file->bp   = file->data;
+
+	return new_file(file, ctx);
+}
+
+static obj_t
+file_new_filter(struct script *ctx,
+		obj_t src,
+		const struct file_filter_funcs *funcs,
+		void *data)
+{
+	struct file *file = script_slab_alloc (ctx, sizeof (struct file));
+
+	file->type = FILTER;
+	file->data = data;
+	file->filter = funcs;
+	file->src = obj_as_file(ctx, src).file;
+
+	return new_file(file, ctx);
+}
+
+
+#if 0
+obj_t file_new_from_stream(struct script *ctx, FILE *file);
+obj_t file_new_from_procedure(struct script *ctx, obj_t src)
+#endif
+
+typedef struct _ascii85_decode_data {
+	uint8_t buf[CHUNK_SIZE];
+	uint8_t *bp;
+	short bytes_available;
+	short eod;
+} _ascii85_decode_data_t;
+
+static int _getc_skip_whitespace (struct file *src)
+{
+	int c;
+
+	do switch ((c = file_getc (src))) {
+	case 0x0:
+	case 0x9:
+	case 0xa:
+	case 0xc:
+	case 0xd:
+	case 0x20:
+		continue;
+
+	default:
+		return c;
+	} while (true);
+
+	return c;
+}
+
+static void _ascii85_decode (struct file *file)
+{
+	_ascii85_decode_data_t *data = file->data;
+	unsigned int n;
+
+	if (data->eod)
+		return;
+
+	data->bp = data->buf;
+
+	n = 0;
+	do {
+		unsigned int v = _getc_skip_whitespace (file->src);
+		if (v == 'z') {
+			data->buf[n+0] = 0;
+			data->buf[n+1] = 0;
+			data->buf[n+2] = 0;
+			data->buf[n+3] = 0;
+		} else if (v == '~') {
+			_getc_skip_whitespace (file->src); /* == '>' || IO_ERROR */
+			data->eod = true;
+			break;
+		} else if (v < '!' || v > 'u') {
+			/* IO_ERROR */
+			data->eod = true;
+			break;
+		} else {
+			unsigned int i;
+
+			v -= '!';
+			for (i = 1; i < 5; i++) {
+				int c = _getc_skip_whitespace (file->src);
+				if (c == '~') { /* short tuple */
+					_getc_skip_whitespace (file->src); /* == '>' || IO_ERROR */
+					data->eod = true;
+					switch (i) {
+					case 0:
+					case 1:
+						/* IO_ERROR */
+						break;
+					case 2:
+						v = v * (85*85*85) + 85*85*85 -1;
+						goto odd1;
+					case 3:
+						v = v * (85*85) + 85*85 -1;
+						goto odd2;
+					case 4:
+						v = v * 85 + 84;
+						data->buf[n+2] = v >> 8 & 0xff;
+odd2:
+						data->buf[n+1] = v >> 16 & 0xff;
+odd1:
+						data->buf[n+0] = v >> 24 & 0xff;
+						data->bytes_available = n + i - 1;
+						return;
+					}
+					break;
+				}
+				v = 85*v + c-'!';
+			}
+
+			data->buf[n+0] = v >> 24 & 0xff;
+			data->buf[n+1] = v >> 16 & 0xff;
+			data->buf[n+2] = v >> 8 & 0xff;
+			data->buf[n+3] = v >> 0 & 0xff;
+		}
+		n += 4;
+	} while (n < sizeof (data->buf) && !data->eod);
+
+	data->bytes_available = n;
+}
+
+static int _ascii85_decode_getc (struct file *file)
+{
+	_ascii85_decode_data_t *data = file->data;
+
+	if (data->bytes_available == 0) {
+		_ascii85_decode (file);
+
+		if (data->bytes_available == 0)
+			return EOF;
+	}
+
+	data->bytes_available--;
+	return *data->bp++;
+}
+
+static void _ascii85_decode_putc (struct file *file, int c)
+{
+	_ascii85_decode_data_t *data = file->data;
+	data->bytes_available++;
+	data->bp--;
+}
+
+static int _ascii85_decode_read (struct file *file, uint8_t *buf, int len)
+{
+	_ascii85_decode_data_t *data = file->data;
+
+	if (data->bytes_available == 0) {
+		_ascii85_decode (file);
+
+		if (data->bytes_available == 0)
+			return 0;
+	}
+
+	if (len > data->bytes_available)
+		len = data->bytes_available;
+	memcpy (buf, data->bp, len);
+	data->bp += len;
+	data->bytes_available -= len;
+	return len;
+}
+
+obj_t file_new_ascii85_decode (struct script *ctx,
+			       struct dictionary *dict,
+			       obj_t src)
+{
+	static const struct file_filter_funcs funcs = {
+		_ascii85_decode_getc,
+		_ascii85_decode_putc,
+		_ascii85_decode_read,
+		script_free,
+	};
+	_ascii85_decode_data_t *data;
+
+	data = script_alloc0 (ctx, 1, sizeof (_ascii85_decode_data_t));
+	return file_new_filter (ctx, src, &funcs, data);
+}
+
+#if HAVE_ZLIB
+#include <zlib.h>
+
+typedef struct _deflate_decode_data {
+	z_stream zlib_stream;
+
+	uint8_t in[CHUNK_SIZE];
+	uint8_t out[CHUNK_SIZE];
+
+	int bytes_available;
+	uint8_t *bp;
+} _deflate_decode_data_t;
+
+static void _deflate_decode (struct file *file)
+{
+	_deflate_decode_data_t *data = file->data;
+	uint8_t *bp;
+	int len;
+
+	data->zlib_stream.next_out = data->out;
+	data->zlib_stream.avail_out = sizeof (data->out);
+
+	bp = data->in;
+	len = sizeof (data->in);
+	if (data->zlib_stream.avail_in) {
+		memmove (data->in,
+			 data->zlib_stream.next_in,
+			 data->zlib_stream.avail_in);
+		len -= data->zlib_stream.avail_in;
+		bp += data->zlib_stream.avail_in;
+	}
+
+	len = file_read (file->src, bp, len);
+
+	data->zlib_stream.next_in = data->in;
+	data->zlib_stream.avail_in += len;
+
+	inflate (&data->zlib_stream, len == 0 ? Z_FINISH : Z_NO_FLUSH);
+
+	data->bytes_available = data->zlib_stream.next_out - data->out;
+	data->bp = data->out;
+}
+
+static int _deflate_decode_getc (struct file *file)
+{
+	_deflate_decode_data_t *data = file->data;
+
+	if (data->bytes_available == 0) {
+		_deflate_decode (file);
+
+		if (data->bytes_available == 0)
+			return EOF;
+	}
+
+	data->bytes_available--;
+	return *data->bp++;
+}
+
+static void _deflate_decode_putc (struct file *file, int c)
+{
+	_deflate_decode_data_t *data = file->data;
+	data->bytes_available++;
+	data->bp--;
+}
+
+static int _deflate_decode_read (struct file *file, uint8_t *buf, int len)
+{
+	_deflate_decode_data_t *data = file->data;
+
+	if (data->bytes_available == 0) {
+		_deflate_decode (file);
+
+		if (data->bytes_available == 0)
+			return 0;
+	}
+
+	if (len > (int) data->bytes_available)
+		len = data->bytes_available;
+	memcpy (buf, data->bp, len);
+	data->bp += len;
+	data->bytes_available -= len;
+	return len;
+}
+
+static void _deflate_destroy (struct script *ctx, void *closure)
+{
+	_deflate_decode_data_t *data;
+
+	data = closure;
+
+	inflateEnd (&data->zlib_stream);
+
+	script_free (ctx, data);
+}
+
+obj_t file_new_deflate_decode(struct script *ctx,
+			      obj_t obj,
+			      struct dictionary *dict)
+{
+	static const struct file_filter_funcs funcs = {
+		_deflate_decode_getc,
+		_deflate_decode_putc,
+		_deflate_decode_read,
+		_deflate_destroy,
+	};
+	_deflate_decode_data_t *data;
+
+	data = script_alloc (ctx, 1, sizeof (_deflate_decode_data_t));
+	data->zlib_stream.zalloc = Z_NULL;
+	data->zlib_stream.zfree = Z_NULL;
+	data->zlib_stream.opaque = Z_NULL;
+	data->zlib_stream.next_in = data->in;
+	data->zlib_stream.avail_in = 0;
+	data->zlib_stream.next_out = data->out;
+	data->zlib_stream.avail_out = sizeof (data->out);
+	data->bytes_available = 0;
+
+	if (inflateInit(&data->zlib_stream) != Z_OK) {
+		script_free(ctx, data);
+		longjmp(ctx->error, -ENOMEM);
+	}
+
+	return file_new_filter(ctx, src, &funcs, data);
+}
+#endif
+
+void file_execute (struct file *obj)
+{
+	scan_file(obj);
+}
+
+int file_getc (struct file *file)
+{
+	int c;
+
+	if (!file->src)
+		return EOF;
+
+	switch (file->type) {
+	case STDIO:
+		if (file->rem) {
+			c = *file->bp++;
+			file->rem--;
+		} else {
+			file->rem = fread (file->bp = file->data, 1, CHUNK_SIZE, file->src);
+		case BYTES:
+			if (file->rem) {
+				c = *file->bp++;
+				file->rem--;
+			} else
+				c = EOF;
+		}
+		break;
+
+	case PROCEDURE:
+		c = EOF;
+		break;
+
+	case FILTER:
+		c = file->filter->filter_getc (file);
+		break;
+
+	default:
+		c = EOF;
+		break;
+	}
+
+	return c;
+}
+
+int file_read (struct file *file, void *buf, int len)
+{
+	int ret;
+
+	if (!file->src)
+		longjmp(file->base.ctx->error, -EINVAL);
+
+	switch (file->type) {
+	case STDIO:
+		if (file->rem > 0) {
+			ret = len;
+			if (file->rem < ret)
+				ret = file->rem;
+			memcpy (buf, file->bp, ret);
+			file->bp  += ret;
+			file->rem -= ret;
+		} else
+			ret = fread (buf, 1, len, file->src);
+		break;
+
+	case BYTES:
+		if (file->rem > 0) {
+			ret = len;
+			if (file->rem < ret)
+				ret = file->rem;
+			memcpy (buf, file->bp, ret);
+			file->bp  += ret;
+			file->rem -= ret;
+		} else
+			ret = 0;
+		break;
+
+	case PROCEDURE:
+		ret = 0;
+		break;
+
+	case FILTER:
+		ret = file->filter->filter_read (file, buf, len);
+		break;
+
+	default:
+		ret = 0;
+		break;
+	}
+
+	return ret;
+}
+
+void file_putc (struct file *file, int c)
+{
+	if (file->src == NULL)
+		return;
+
+	switch ((int) file->type) {
+	case STDIO:
+	case BYTES:
+		file->bp--;
+		file->rem++;
+		break;
+	case FILTER:
+		file->filter->filter_putc (file, c);
+		break;
+	default:
+		break;
+	}
+}
+
+void file_flush (struct file *file)
+{
+	if (file->src == NULL)
+		return;
+
+	switch ((int) file->type) {
+	case FILTER: /* need to eat EOD */
+		while (file_getc (file) != EOF)
+			;
+		break;
+	default:
+		break;
+	}
+}
+
+void file_close(struct file *file)
+{
+	if (file->src == NULL)
+		return;
+
+	switch (file->type) {
+	case STDIO:
+		if (file->flags & OWN_STREAM)
+			fclose (file->src);
+		break;
+	case BYTES:
+		if (file->src != file->data) {
+			struct string *src = file->src;
+			if (src != NULL && --src->base.ref == 0)
+				string_free(src);
+		}
+		break;
+	case FILTER:
+		{
+			struct file *src = file->src;
+			if (src != NULL && --src->base.ref == 0)
+				file_free(src);
+		}
+		break;
+	case PROCEDURE:
+	default:
+		break;
+	}
+	file->src = NULL;
+}
+
+void file_free(struct file *file)
+{
+	file_flush(file);
+	/* XXX putback */
+	file_close(file);
+
+	switch (file->type) {
+	case BYTES:
+		break;
+	case PROCEDURE:
+		break;
+	case STDIO:
+		script_free (file->base.ctx, file->data);
+		break;
+	case FILTER:
+		file->filter->filter_destroy (file->base.ctx, file->data);
+		break;
+	default:
+		break;
+	}
+
+	script_slab_free(file->base.ctx, file, sizeof (struct file));
+}
+
+obj_t file_as_string(struct file *file)
+{
+	char *bytes;
+	unsigned int len;
+	unsigned int allocated;
+
+	allocated = 16384;
+	bytes = script_alloc(file->base.ctx, 1, allocated);
+
+	len = 0;
+	do {
+		int ret;
+
+		ret = file_read(file, bytes + len, allocated - len);
+		if (ret == 0)
+			break;
+
+		len += ret;
+		if (len + 1 > allocated / 2) {
+			bytes = script_realloc(file->base.ctx, bytes,
+					       allocated, 2);
+			allocated *= 2;
+		}
+	} while (true);
+
+	bytes[len] = '\0'; /* better safe than sorry! */
+	return string_new_from_bytes(file->base.ctx, bytes, len);
+}
diff --git a/fuzz/gemscript.c b/fuzz/gemscript.c
new file mode 100644
index 0000000..53099d2
--- /dev/null
+++ b/fuzz/gemscript.c
@@ -0,0 +1,1088 @@
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/shm.h>
+
+#include "script.h"
+
+#include "igt.h"
+
+#include "ioctl_wrappers.h"
+#include "igt_kcov.h"
+
+#define check(CNT) ostack_check(ctx, CNT)
+#define pop(CNT) ostack_pop(ctx, (CNT))
+#define push(OBJ) ostack_push(ctx, (OBJ))
+
+#define ERR(expr) do if ((expr)) longjmp(ctx->error, -EINVAL); while(0)
+
+#define gem_ctx(G) ((G)->base.heap.ctx)
+#define gem_error(G, E) longjmp(gem_ctx(G)->error, E)
+
+#define LOCAL_I915_EXEC_NO_RELOC (1<<11)
+#define LOCAL_I915_EXEC_HANDLE_LUT (1<<12)
+
+struct gem_batch;
+
+struct private {
+	struct {
+		name_t engine, engines;
+		name_t context;
+		name_t width;
+		name_t height;
+		name_t pitch;
+		name_t tiling;
+		name_t pinned;
+		name_t size;
+		name_t caching;
+	} name;
+};
+
+static struct dictionary *
+ostack_get_dictionary(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+
+	ERR((obj_type(obj) != OBJ_TYPE_DICTIONARY));
+
+	return obj.dictionary;
+}
+
+static struct string *
+ostack_get_string(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+
+	/* XXX to string */
+	ERR((obj_type(obj) != OBJ_TYPE_STRING));
+
+	return obj.string;
+}
+
+static long ostack_get_integer(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+	switch (obj_type(obj)) {
+	case OBJ_TYPE_BOOLEAN:
+		return obj.boolean;
+	case OBJ_TYPE_INTEGER:
+		return obj.integer;
+	case OBJ_TYPE_REAL:
+		return obj.real;
+	default:
+		ERR(1);
+	}
+}
+
+static long
+dictionary_get_integer(struct dictionary *dict, name_t name, long optional)
+{
+	obj_t obj = dictionary_get(dict, name);
+	switch (obj_type(obj)) {
+	case OBJ_TYPE_BOOLEAN:
+		return obj.boolean;
+	case OBJ_TYPE_INTEGER:
+		return obj.integer;
+	case OBJ_TYPE_REAL:
+		return obj.real;
+	default:
+		return optional;
+	}
+}
+
+enum {
+	GEM_TYPE_DRIVER = OBJ_TYPE_EXTENSION_0,
+	GEM_TYPE_CONTEXT,
+	GEM_TYPE_OBJECT,
+	GEM_TYPE_BATCH,
+};
+
+struct gem {
+	struct extension_object base;
+	int fd, gen, vendor;
+
+	unsigned engines[16];
+	unsigned nengine;
+};
+
+static struct gem *
+ostack_get_gem(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+
+	ERR((obj_type(obj) != GEM_TYPE_DRIVER));
+
+	obj.object->ref++;
+	return obj.ptr;
+}
+
+static void gem_free(void *ptr)
+{
+	struct gem *gem = ptr;
+	close(gem->fd);
+	script_free(gem_ctx(gem), gem);
+}
+
+static void gem_put(struct gem *gem)
+{
+	if (--gem->base.heap.ref == 0)
+		gem_free(gem);
+}
+
+static const struct extension gem_ops = {
+	.free = gem_free,
+};
+
+static const char *dictionary_get_string(struct dictionary *dict, const char *key)
+{
+	obj_t obj;
+
+	obj = dictionary_get(dict, name_new_static(dict->base.ctx, key).name);
+	if (obj_type(obj) != OBJ_TYPE_STRING)
+		longjmp(dict->base.ctx->error, -EINVAL);
+
+	return obj.string->string;
+}
+
+static int lookup_vendor(const char *str)
+{
+	if (!str)
+		return DRIVER_INTEL;
+
+	if (strcmp(str, "intel") == 0)
+		return DRIVER_INTEL;
+
+	return DRIVER_INTEL;
+}
+
+static void gem_new(struct script *ctx)
+{
+	obj_t obj;
+	struct gem *gem;
+	const char *vendor = NULL;
+
+	gem = script_alloc0(ctx, 1, sizeof(*gem));
+
+	check(1);
+
+	obj = ostack_get(ctx, 0);
+	switch (obj_type(obj)) {
+	case OBJ_TYPE_STRING:
+		vendor = obj.string->string;
+		break;
+	case OBJ_TYPE_DICTIONARY:
+		vendor = dictionary_get_string(obj.dictionary, "vendor");
+		break;
+	default:
+		ERR(1);
+		break;
+	}
+
+	pop(1);
+
+	gem->vendor = lookup_vendor(vendor);
+	gem->fd = drm_open_driver(gem->vendor);
+	if (gem->vendor == DRIVER_INTEL)
+		gem-> gen = intel_gen(intel_get_drm_devid(gem->fd));
+
+	push(new_extension_object(&gem->base, ctx, GEM_TYPE_DRIVER, &gem_ops));
+}
+
+struct gem_object {
+	struct extension_object base;
+	struct gem *gem;
+	uint32_t handle;
+	long width, height, pitch, size;
+	long alignment, offset;
+	unsigned flags;
+	int tiling, caching;
+	uint32_t *expected;
+
+	uint32_t *shadow;
+	bool dirty;
+
+	struct drm_i915_gem_exec_object2 *obj;
+};
+
+static struct gem_object *
+ostack_get_object(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+
+	ERR((obj_type(obj) != GEM_TYPE_OBJECT));
+
+	return obj.ptr;
+}
+
+static void gem_object_free(void *ptr)
+{
+	struct gem_object *go = ptr;
+	struct gem *gem = go->gem;
+
+	munmap(go->shadow, go->size);
+
+	gem_close(gem->fd, go->handle);
+	script_free(gem_ctx(go), go);
+
+	gem_put(gem);
+}
+
+static const struct extension gem_object_ops = {
+	.free = gem_object_free,
+};
+
+struct gem_context {
+	struct extension_object base;
+	struct gem *gem;
+	uint32_t handle;
+};
+
+static void gem_context_free(void *ptr)
+{
+	struct gem_context *gc = ptr;
+	struct gem *gem = gc->gem;
+
+	gem_context_destroy(gem->fd, gc->handle);
+	script_free(gem_ctx(gc), gc);
+
+	gem_put(gem);
+}
+
+static const struct extension gem_context_ops = {
+	.free = gem_context_free,
+};
+
+static void gem_object_new(struct script *ctx)
+{
+	struct private *ext = ctx->private;
+	struct dictionary *dict;
+	struct gem_object *go;
+	uint32_t shadow;
+
+	go = script_alloc0(ctx, 1, sizeof(*go));
+
+	check(2);
+
+	dict = ostack_get_dictionary(ctx, 0);
+	go->gem = ostack_get_gem(ctx, 1);
+
+	go->width = dictionary_get_integer(dict, ext->name.width, 0);
+	go->height = dictionary_get_integer(dict, ext->name.height, 0);
+	go->pitch = dictionary_get_integer(dict, ext->name.pitch, 0);
+	if (go->pitch == 0)
+		go->pitch = go->width * 4;
+	go->size = go->pitch * go->height;
+	if (go->size == 0)
+		go->size = dictionary_get_integer(dict, ext->name.size, 0);
+	ERR(go->size == 0);
+
+	if (dictionary_has(dict, ext->name.pinned)) {
+		go->offset = dictionary_get_integer(dict, ext->name.pinned, 0);
+		go->flags |= EXEC_OBJECT_PINNED;
+	}
+
+	go->size = (go->size + 4095) & -4096;
+	go->handle = gem_create(go->gem->fd, go->size);
+
+	go->tiling = dictionary_get_integer(dict, ext->name.tiling, I915_TILING_NONE);
+	if (go->tiling)
+		gem_set_tiling(go->gem->fd, go->handle, go->tiling, go->pitch);
+
+	go->caching = dictionary_get_integer(dict, ext->name.caching, -1);
+	if (go->caching != -1)
+		gem_set_caching(go->gem->fd, go->handle, go->caching);
+
+	shadow = gem_create(go->gem->fd, go->size);
+	go->shadow = gem_mmap__cpu(go->gem->fd, shadow, 0, go->size, PROT_WRITE);
+	gem_set_domain(go->gem->fd, shadow, I915_GEM_DOMAIN_CPU, I915_GEM_DOMAIN_CPU);
+	gem_close(go->gem->fd, shadow);
+
+	pop(1);
+
+	push(new_extension_object(&go->base, ctx, GEM_TYPE_OBJECT, &gem_object_ops));
+}
+
+static void gem_object_read(struct script *ctx)
+{
+	struct gem_object *go;
+	long offset, len;
+	char *buf;
+
+	check(3);
+
+	go = ostack_get_object(ctx, 2);
+	offset = ostack_get_integer(ctx, 1);
+	len = ostack_get_integer(ctx, 0);
+
+	if (offset < 0 || offset + len > go->size)
+		longjmp(ctx->error, -ERANGE);
+
+	buf = script_alloc(ctx, 1, len);
+	gem_read(go->gem->fd, go->handle, offset, buf, len);
+
+	pop(3);
+	push(string_new_from_bytes(ctx, buf, len));
+}
+
+static void gem_object_write(struct script *ctx)
+{
+	struct gem_object *go;
+	obj_t src;
+	long offset;
+
+	check(3);
+
+	go = ostack_get_object(ctx, 2);
+	offset = ostack_get_integer(ctx, 1);
+	src = ostack_get(ctx, 0);
+
+	if (obj_type(src) == OBJ_TYPE_STRING) {
+		struct string *str = ostack_get_string(ctx, 0);
+
+		if (offset < 0 || offset + str->len > go->size)
+			longjmp(ctx->error, -ERANGE);
+
+		memcpy((char *)go->shadow + offset, str->string, str->len);
+		go->dirty = true;
+
+		gem_write(go->gem->fd, go->handle, offset, str->string, str->len);
+	} else if (obj_is_number(src)) {
+		uint32_t value = number_get_value(src);
+
+		if (offset < 0 || offset >= go->size / sizeof(uint32_t))
+			longjmp(ctx->error, -ERANGE);
+
+		go->shadow[offset] = value;
+		go->dirty = true;
+
+		gem_write(go->gem->fd, go->handle,
+			  offset*sizeof(uint32_t), &value, sizeof(value));
+	} else {
+		ERR(1);
+	}
+
+	pop(3);
+}
+
+static void gem_object_verify(struct script *ctx)
+{
+	struct gem_object *go;
+	uint32_t *map;
+
+	check(1);
+
+	go = ostack_get_object(ctx, 0);
+
+	if (go->dirty) {
+		map = gem_mmap__cpu(go->gem->fd, go->handle, 0, go->size, PROT_READ);
+		gem_set_domain(go->gem->fd, go->handle, I915_GEM_DOMAIN_CPU, 0);
+		for (long n = 0; n < go->size/4; n++) {
+			if (go->shadow[n] != map[n]) {
+				fprintf(stderr,
+						"discrepancy found in GEM object %d, at offset %ld: expected %x, found %x\n",
+						go->handle, n, go->shadow[n], map[n]);
+				abort();
+			}
+		}
+		munmap(map, go->size);
+		go->dirty = false;
+	}
+
+	pop(1);
+}
+
+static void gem_object_unref(struct gem_object *go)
+{
+	if (--go->base.heap.ref == 0)
+		gem_object_free(go);
+}
+
+static struct gem_object *gem_object_ref(struct gem_object *go)
+{
+	go->base.heap.ref++;
+	return go;
+}
+
+static void gem_context_new(struct script *ctx)
+{
+	struct gem_context *gc;
+
+	gc = script_alloc0(ctx, 1, sizeof(*gc));
+
+	check(1);
+	gc->gem = ostack_get_gem(ctx, 0);
+	gc->handle = gem_context_create(gc->gem->fd);
+	pop(1);
+
+	push(new_extension_object(&gc->base, ctx, GEM_TYPE_CONTEXT, &gem_context_ops));
+}
+
+struct gem_batch {
+	struct extension_object base;
+	struct gem *gem;
+
+	struct drm_i915_gem_exec_object2 batch_obj;
+	uint32_t *batch;
+
+	struct drm_i915_gem_exec_object2 *obj;
+	struct gem_object **go;
+
+	long nobj;
+	long nbatch, size;
+};
+
+static struct gem_batch *
+ostack_get_batch(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+
+	ERR((obj_type(obj) != GEM_TYPE_BATCH));
+
+	return obj.ptr;
+}
+
+static void gem_batch_free(void *ptr)
+{
+	struct gem_batch *gb = ptr;
+	struct gem *gem = gb->gem;
+	
+	for (long n = 0; n < gb->nobj - 1; n++) {
+		if (gb->go[n]->gem != gb->gem)
+			gem_close(gb->gem->fd, gb->obj[n].handle);
+		gem_object_unref(gb->go[n]);
+	}
+
+	script_free(gem_ctx(gb), gb->obj);
+	script_free(gem_ctx(gb), gb->go);
+
+	munmap(gb->batch, gb->size);
+	gem_close(gem->fd, gb->batch_obj.handle);
+
+	script_free(gem_ctx(gb), gb);
+
+	gem_put(gem);
+}
+
+static const struct extension gem_batch_ops = {
+	.free = gem_batch_free,
+};
+
+static struct drm_i915_gem_exec_object2 *
+__gem_batch_add_object(struct gem_batch *gb)
+{
+	if ((gb->nobj & -gb->nobj) == gb->nobj) {
+		int len = 2*gb->nobj;
+		if (len == 0)
+			len = 1;
+		gb->obj = script_realloc(gem_ctx(gb), gb->obj,
+					 len, sizeof(*gb->obj));
+		gb->go = script_realloc(gem_ctx(gb), gb->go,
+				       	len, sizeof(*gb->go));
+	}
+
+	return memset(&gb->obj[gb->nobj++], 0, sizeof(*gb->obj));
+}
+
+static struct drm_i915_gem_exec_object2 *
+gem_batch_add_object(struct gem_batch *gb, struct gem_object *go)
+{
+	struct drm_i915_gem_exec_object2 *obj;
+
+	if (go->obj)
+		return go->obj;
+
+	obj = __gem_batch_add_object(gb);
+	obj->handle = go->handle;
+	obj->alignment = go->alignment;
+	obj->offset = go->offset;
+	obj->flags = go->flags;
+
+	if (go->gem != gb->gem) {
+		uint32_t name = gem_flink(go->gem->fd, go->handle);
+		obj->handle = gem_open(gb->gem->fd, name);
+	}
+
+	go->obj = obj;
+	gb->go[obj - gb->obj] = gem_object_ref(go);
+
+	return obj;
+}
+
+static struct drm_i915_gem_relocation_entry *
+gem_batch_add_reloc(struct gem_batch *gb,
+		    struct drm_i915_gem_exec_object2 *obj,
+		    struct drm_i915_gem_exec_object2 *target)
+{
+	struct drm_i915_gem_relocation_entry *reloc;
+
+	if ((obj->relocation_count & -obj->relocation_count) == obj->relocation_count) {
+		int len = 2*obj->relocation_count;
+		if (len == 0)
+			len = 1;
+
+		reloc = (typeof(reloc))(uintptr_t)obj->relocs_ptr;
+		reloc = script_realloc(gem_ctx(gb), reloc, len, sizeof(*reloc));
+		obj->relocs_ptr = (uintptr_t)reloc;
+	}
+
+	reloc = (typeof(reloc))(uintptr_t)obj->relocs_ptr;
+	reloc = memset(reloc + obj->relocation_count, 0, sizeof(*reloc));
+	reloc->target_handle = target - gb->obj;
+	reloc->presumed_offset = target->offset;
+	obj->relocation_count++;
+
+	return reloc;
+}
+
+static uint32_t *
+gem_batch_add(struct gem_batch *gb, long count)
+{
+	uint32_t *ptr;
+
+	if (gb->nbatch + count > gb->size/sizeof(uint32_t)) {
+		uint32_t handle = gem_create(gb->gem->fd, gb->size*2);
+		uint32_t *map = gem_mmap__cpu(gb->gem->fd, handle,
+					      0, gb->size*2,
+					      PROT_WRITE);
+		memcpy(map, gb->batch, gb->nbatch*sizeof(uint32_t));
+		munmap(gb->batch, gb->size);
+
+		gb->batch = map;
+		gb->size *= 2;
+	}
+
+	ptr = gb->batch + gb->nbatch;
+	gb->nbatch += count;
+
+	return ptr;
+}
+
+static long
+gem_batch_offset(struct gem_batch *gb, uint32_t *ptr)
+{
+	return (ptr - gb->batch) * sizeof(uint32_t);
+}
+
+static void __gem_batch_store(struct gem_batch *gb,
+			      struct gem_object *go,
+			      long offset, long value)
+{
+	struct drm_i915_gem_relocation_entry *reloc;
+	struct drm_i915_gem_exec_object2 *obj;
+	uint32_t *batch;
+	int i;
+
+	if (offset < 0 || offset >= go->size/sizeof(uint32_t))
+		gem_error(gb, -ERANGE);
+
+	/* XXX delayed vs multiple execution */
+	go->shadow[offset] = value;
+	go->dirty = true;
+	offset *= sizeof(uint32_t);
+
+	obj = gem_batch_add_object(gb, go);
+	reloc = gem_batch_add_reloc(gb, &gb->batch_obj, obj);
+	batch = gem_batch_add(gb, 4);
+
+	reloc->offset = gem_batch_offset(gb, batch) + sizeof(uint32_t);
+	reloc->delta = offset;
+	reloc->read_domains = I915_GEM_DOMAIN_INSTRUCTION;
+	reloc->write_domain = I915_GEM_DOMAIN_INSTRUCTION;
+	obj->flags |= EXEC_OBJECT_WRITE;
+
+	offset += obj->offset;
+
+	i = 0;
+	batch[i] = MI_STORE_DWORD_IMM | (gb->gem->gen < 6 ? 1 << 22 : 0);
+	if (gb->gem->gen >= 8) {
+		batch[++i] = offset;
+		batch[++i] = offset >> 32;
+	} else if (gb->gem->gen >= 4) {
+		batch[++i] = 0;
+		batch[++i] = offset;
+		reloc->offset += sizeof(uint32_t);
+	} else {
+		batch[i]--;
+		batch[++i] = offset;
+	}
+	batch[++i] = value;
+	while (++i < 4)
+		batch[i] = 0;
+}
+
+static void gem_batch_store(struct script *ctx)
+{
+	struct gem_batch *gb;
+	struct gem_object *go;
+	long offset, value;
+
+	check(4);
+
+	gb = ostack_get_batch(ctx, 3);
+	go = ostack_get_object(ctx, 2);
+	offset = ostack_get_integer(ctx, 1);
+	value = ostack_get_integer(ctx, 0);
+
+	__gem_batch_store(gb, go, offset, value);
+
+	pop(3);
+}
+
+static void gem_batch_exec(struct script *ctx)
+{
+	struct private *ext = ctx->private;
+	struct drm_i915_gem_execbuffer2 execbuf;
+	struct gem_batch *gb;
+	struct dictionary *dict;
+	obj_t result;
+
+	check(2);
+
+	gb = ostack_get_batch(ctx, 1);
+	dict = ostack_get_dictionary(ctx, 0);
+
+	memset(&execbuf, 0, sizeof(execbuf));
+	execbuf.buffers_ptr = (uintptr_t)gb->obj;
+	execbuf.buffer_count = gb->nobj;
+	execbuf.flags = dictionary_get_integer(dict, ext->name.engine, 0);
+	execbuf.flags |= LOCAL_I915_EXEC_HANDLE_LUT;
+	execbuf.flags |= LOCAL_I915_EXEC_NO_RELOC;
+
+	result = dictionary_get(dict, ext->name.context);
+	if (obj_type(result) == GEM_TYPE_CONTEXT) {
+		struct gem_context *gc = result.ptr;
+		execbuf.rsvd1 = gc->handle;
+	}
+
+	gem_execbuf(gb->gem->fd, &execbuf);
+
+	for (long n = 0; n < gb->nobj - 1; n++)
+		gb->go[n]->offset = gb->obj[n].offset;
+
+	pop(1);
+}
+
+static void gem_batch_new(struct script *ctx)
+{
+	obj_t proc;
+	struct gem_batch *gb;
+
+	gb = script_alloc0(ctx, 1, sizeof(*gb));
+
+	check(2);
+
+	proc = ostack_copy(ctx, 0);
+	gb->gem = ostack_get_gem(ctx, 1);
+
+	ERR(!(obj_is_procedure(proc) || is_null(proc)));
+
+	gb->size = 4096;
+	gb->batch_obj.handle = gem_create(gb->gem->fd, gb->size);
+	gb->batch = gem_mmap__cpu(gb->gem->fd, gb->batch_obj.handle, 0, gb->size, PROT_WRITE);
+
+	pop(1);
+
+	push(new_extension_object(&gb->base, ctx, GEM_TYPE_BATCH, &gem_batch_ops));
+	gb->base.heap.ref++;
+
+	if (obj_is_procedure(proc))
+		array_execute(proc.array);
+	obj_free(proc);
+
+	for (long n = 0; n < gb->nobj; n++)
+		gb->go[n]->obj = NULL;
+
+	*__gem_batch_add_object(gb) = gb->batch_obj;
+	*gem_batch_add(gb, 1) = MI_BATCH_BUFFER_END;
+
+	if (!--gb->base.heap.ref)
+		gem_batch_free(gb);
+}
+
+static void gem_set(struct gem *gem, name_t key, obj_t value)
+{
+	gem_error(gem, -EINVAL);
+}
+
+static void gem_context_set(struct gem_context *gc, name_t key, obj_t value)
+{
+	gem_error(gc, -EINVAL);
+}
+
+static void gem_object_set(struct gem_object *go, name_t key, obj_t value)
+{
+	struct private *ext = gem_ctx(go)->private;
+
+	if (key == ext->name.tiling) {
+		go->tiling = number_get_value(value);
+		gem_set_tiling(go->gem->fd, go->handle, go->tiling, go->pitch);
+	} else if (key == ext->name.caching) {
+		go->caching = number_get_value(value);
+		gem_set_caching(go->gem->fd, go->handle, go->caching);
+	} else if (key == ext->name.pinned) {
+		if (obj_type(value) != OBJ_TYPE_NULL) {
+			go->offset = number_get_value(value);
+			go->flags |= EXEC_OBJECT_PINNED;
+		} else
+			go->flags &= ~EXEC_OBJECT_PINNED;
+	} else
+		gem_error(go, -EINVAL);
+}
+
+static void any_set(struct script *ctx)
+{
+	obj_t obj, value, key;
+
+	check(3);
+
+	obj = ostack_get(ctx, 2);
+	if (obj_type(obj) < GEM_TYPE_DRIVER) {
+		systemdict_execute(ctx, "set");
+		return;
+	}
+
+	key = ostack_get(ctx, 1);
+	ERR(obj_type(key) != OBJ_TYPE_NAME);
+
+	value = ostack_get(ctx, 0);
+
+	switch (obj_type(obj)) {
+	case GEM_TYPE_DRIVER:
+		gem_set(obj.ptr, key.name, value);
+		break;
+
+	case GEM_TYPE_CONTEXT:
+		gem_context_set(obj.ptr, key.name, value);
+		break;
+
+	case GEM_TYPE_OBJECT:
+		gem_object_set(obj.ptr, key.name, value);
+		break;
+
+	default:
+		ERR(1);
+		break;
+	}
+
+	pop(2);
+}
+
+static void gem_driver_wait(struct gem *gem)
+{
+	gem_quiescent_gpu(gem->fd);
+}
+
+static void gem_object_wait(struct gem_object *go)
+{
+	gem_wait(go->gem->fd, go->handle, NULL);
+}
+
+static void gem_batch_wait(struct gem_batch *gb)
+{
+	gem_wait(gb->gem->fd, gb->batch_obj.handle, NULL);
+}
+
+static void any_wait(struct script *ctx)
+{
+	obj_t obj;
+
+	check(1);
+	obj = ostack_get(ctx, 0);
+	switch (obj_type(obj)) {
+	case GEM_TYPE_DRIVER:
+		gem_driver_wait(obj.ptr);
+		break;
+
+	case GEM_TYPE_OBJECT:
+		gem_object_wait(obj.ptr);
+		break;
+
+	case GEM_TYPE_BATCH:
+		gem_batch_wait(obj.ptr);
+		break;
+
+	default:
+		ERR(1);
+		break;
+	}
+}
+
+static void gem_get_engines(struct gem *gem)
+{
+	obj_t obj;
+
+	if (gem->nengine == 0) {
+		unsigned engine;
+
+		for_each_engine(gem->fd, engine) {
+			if (engine == 0)
+				continue;
+
+			if (gem->gen == 6 && e__->exec_id == I915_EXEC_BSD)
+				continue;
+
+			if (gem_has_bsd2(gem->fd) && engine == I915_EXEC_BSD)
+				continue;
+
+			gem->engines[gem->nengine++] = engine;
+		}
+
+		if (gem->nengine == 0)
+			gem->engines[gem->nengine++] = 0;
+	}
+
+	obj = array_new(gem_ctx(gem), gem->nengine);
+	for (int n = 0; n < gem->nengine; n++)
+		array_append(obj.array, new_int(gem->engines[n]));
+	ostack_push(gem_ctx(gem), obj);
+}
+
+static void gem_get(struct gem *gem, name_t key)
+{
+	struct private *ext = gem_ctx(gem)->private;
+
+	if (key == ext->name.engines)
+		gem_get_engines(gem);
+	else
+		gem_error(gem, -EINVAL);
+}
+
+static void gem_context_get(struct gem_context *gc, name_t key)
+{
+	gem_error(gc, -EINVAL);
+}
+
+static void gem_object_get(struct gem_object *go, name_t key)
+{
+	struct private *ext = gem_ctx(go)->private;
+
+	if (key == ext->name.tiling) {
+		ostack_push(gem_ctx(go), new_int(go->tiling));
+	} else if (key == ext->name.caching) {
+		ostack_push(gem_ctx(go), new_int(go->caching));
+	} else
+		gem_error(go, -EINVAL);
+}
+
+static void any_get(struct script *ctx)
+{
+	obj_t obj, key;
+
+	check(2);
+
+	obj = ostack_get(ctx, 1);
+	if (obj_type(obj) < GEM_TYPE_DRIVER) {
+		systemdict_execute(ctx, "get");
+		return;
+	}
+
+	key = ostack_copy(ctx, 0);
+	ERR(obj_type(key) != OBJ_TYPE_NAME);
+
+	pop(1);
+
+	switch (obj_type(obj)) {
+	case GEM_TYPE_DRIVER:
+		gem_get(obj.ptr, key.name);
+		break;
+
+	case GEM_TYPE_CONTEXT:
+		gem_context_get(obj.ptr, key.name);
+		break;
+
+	case GEM_TYPE_OBJECT:
+		gem_object_get(obj.ptr, key.name);
+		break;
+
+	default:
+		ERR(1);
+		break;
+	}
+
+	obj_free(key);
+}
+
+static const struct odef {
+	const char *name;
+	void (*op)(struct script *);
+} operators[] = {
+	{ "batch", gem_batch_new },
+	{ "context", gem_context_new },
+	{ "driver", gem_new },
+	{ "exec", gem_batch_exec },
+	{ "get", any_get },
+	{ "object", gem_object_new },
+	{ "read", gem_object_read },
+	{ "set", any_set },
+	{ "store", gem_batch_store },
+	{ "verify", gem_object_verify },
+	{ "wait", any_wait },
+	{ "write", gem_object_write },
+	{ NULL },
+};
+
+static void init_extension(struct script *ctx)
+{
+	struct dictionary *dict = extensiondict(ctx);
+	struct private *ext;
+
+	ext = script_alloc(ctx, 1, sizeof(*ext));
+	ctx->private = ext;
+
+#define NAME(N) ext->name.N = name_new_static(ctx, #N).name
+	NAME(engine);
+	NAME(engines);
+	NAME(context);
+	NAME(width);
+	NAME(height);
+	NAME(pitch);
+	NAME(tiling);
+	NAME(size);
+	NAME(caching);
+	NAME(pinned);
+#undef NAME
+
+	for (const struct odef *def = operators; def->name; def++)
+		dictionary_set(dict,
+			       name_new_static(ctx, def->name).name,
+			       new_operator(def->op));
+}
+
+static int gem_run(struct script *ctx, const char *filename)
+{
+	int ret;
+
+	intel_detect_and_clear_missed_interrupts(-1);
+	igt_kcov_start();
+
+	ret = script_run(ctx, filename);
+	if (ret)
+		fprintf(stderr, "%s(%s) => %d\n", __func__, filename, ret);
+
+	igt_kcov_stop();
+	igt_assert_eq(intel_detect_and_clear_missed_interrupts(-1), 0);
+
+	return ret;
+}
+
+#define AFL_MAP_SIZE_POW2       16
+#define AFL_MAP_SIZE            (1 << AFL_MAP_SIZE_POW2)
+#define AFL_SHM_ID		"__AFL_SHM_ID"
+#define AFL_FORKSRV		198
+
+static inline uint32_t hash_64(uint64_t val, unsigned int bits)
+{
+#define GOLDEN_RATIO_64 0x61C8864680B583EBull
+	return val * GOLDEN_RATIO_64 >> (64 - bits);
+}
+
+static void kcov_report(struct igt_kcov *kcov, void *data)
+{
+	uint8_t *afl = data;
+	const unsigned long n = __atomic_load_n(&kcov->bb[0], __ATOMIC_RELAXED);
+	unsigned prev = 0;
+
+	for (unsigned long i = 0; i < n; i++) {
+		unsigned bb = hash_64(kcov->bb[i+1], AFL_MAP_SIZE_POW2);
+		afl[bb ^ prev]++;
+		prev = hash_64(kcov->bb[i+1] + 1, AFL_MAP_SIZE_POW2);
+	}
+}
+
+static void setup_afl(void)
+{
+	const char *env;
+	bool stopped = false;
+	uint8_t *afl;
+	uint32_t ack;
+
+	env = getenv(AFL_SHM_ID);
+	if (!env)
+		return;
+
+	afl = shmat(atoi(env), NULL, 0);
+	if (afl == (void *)-1)
+		return;
+
+	afl[0] = 1; /* hello world! */
+	igt_kcov_setup(kcov_report, afl);
+
+	ack = 0;
+	if (write(AFL_FORKSRV + 1,  &ack, sizeof(ack)) != sizeof(ack))
+		return;
+
+	for (;;) {
+		uint32_t pid;
+		int status;
+
+		if (read(AFL_FORKSRV, &ack, sizeof(ack)) != sizeof(ack))
+			break;
+
+		if (stopped && ack) {
+			if (waitpid(pid, &status, 0) == -1)
+				break;
+			stopped = false;
+		}
+		if (stopped) {
+			kill(pid, SIGCONT);
+			stopped = false;
+		} else {
+			pid = fork();
+			if (!pid) { /* child */
+				close(AFL_FORKSRV);
+				close(AFL_FORKSRV + 1);
+				return;
+			}
+		}
+		if (write(AFL_FORKSRV+ 1, &pid, 4) != 4)
+			break;
+
+		if (waitpid(pid, &status, WUNTRACED) == -1)
+			break;
+
+		stopped = WIFSTOPPED(status);
+		if (write(AFL_FORKSRV + 1, &status, 4) != 4)
+			break;
+	}
+
+	exit(1);
+}
+
+int main(int argc, char **argv)
+{
+	struct script ctx;
+	int ret;
+
+	script_init(&ctx);
+	init_extension(&ctx);
+
+	/* Last action before running scripts.
+	 * AFL utilizes a forkserver so that initialisation is done once,
+	 * then subsequent tests are executed using the Copy-on-Write process
+	 * image.
+	 */
+	setup_afl();
+
+	if (argc == 1)
+		return gem_run(&ctx, NULL);
+
+	for (int i = 1; i < argc; i++) {
+		switch (argc > 1 ? fork() : 0) {
+		case -1:
+			return -1;
+		case 0:
+			exit(gem_run(&ctx, argv[i]));
+		default:
+			if (wait(&ret) == -1)
+				return -1;
+			if (WIFSIGNALED(ret))
+				raise(WTERMSIG(ret));
+			if (!WIFEXITED(ret))
+				return -1;
+			if (WEXITSTATUS(ret))
+				return WEXITSTATUS(ret);
+		}
+	}
+
+	script_fini(&ctx);
+	return 0;
+}
diff --git a/fuzz/hash.c b/fuzz/hash.c
new file mode 100644
index 0000000..427f55a
--- /dev/null
+++ b/fuzz/hash.c
@@ -0,0 +1,398 @@
+/* 
+ * Copyright © 2004 Red Hat, Inc.
+ * Copyright © 2005 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it either under the terms of the GNU Lesser General Public
+ * License version 2.1 as published by the Free Software Foundation
+ * (the "LGPL") or, at your option, under the terms of the Mozilla
+ * Public License Version 1.1 (the "MPL"). If you do not alter this
+ * notice, a recipient may use your version of this file under either
+ * the MPL or the LGPL.
+ *
+ * You should have received a copy of the LGPL along with this library
+ * in the file COPYING-LGPL-2.1; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA
+ * You should have received a copy of the MPL along with this library
+ * in the file COPYING-MPL-1.1
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY
+ * OF ANY KIND, either express or implied. See the LGPL or the MPL for
+ * the specific language governing rights and limitations.
+ *
+ * The Original Code is the cairo graphics library.
+ *
+ * The Initial Developer of the Original Code is Red Hat, Inc.
+ *
+ * Contributor(s):
+ *      Keith Packard <keithp@xxxxxxxxxx>
+ *	Graydon Hoare <graydon@xxxxxxxxxx>
+ *	Carl Worth <cworth@xxxxxxxxxx>
+ *	Karl Tomlinson <karlt+@xxxxxxxxx>, Mozilla Corporation
+ */
+
+#include "script.h"
+
+#include <stdlib.h>
+
+/*
+ * An entry can be in one of three states:
+ *
+ * FREE: Entry has never been used, terminates all searches.
+ *       Appears in the table as a %NULL pointer.
+ *
+ * DEAD: Entry had been live in the past. A dead entry can be reused
+ *       but does not terminate a search for an exact entry.
+ *       Appears in the table as a pointer to DEAD_ENTRY.
+ *
+ * LIVE: Entry is currently being used.
+ *       Appears in the table as any non-%NULL, non-DEAD_ENTRY pointer.
+ */
+
+#define DEAD_ENTRY ((struct hash_entry *) 0x1)
+
+#define ENTRY_IS_FREE(entry) ((entry) == NULL)
+#define ENTRY_IS_DEAD(entry) ((entry) == DEAD_ENTRY)
+#define ENTRY_IS_LIVE(entry) ((entry) >  DEAD_ENTRY)
+
+
+/* This table is open-addressed with double hashing. Each table size is a
+ * prime chosen to be a little more than double the high water mark for a
+ * given arrangement, so the tables should remain < 50% full. The table
+ * size makes for the "first" hash modulus; a second prime (2 less than the
+ * first prime) serves as the "second" hash modulus, which is co-prime and
+ * thus guarantees a complete permutation of table indices.
+ *
+ * This structure, and accompanying table, is borrowed/modified from the
+ * file xserver/render/glyph.c in the freedesktop.org x server, with
+ * permission (and suggested modification of doubling sizes) by Keith
+ * Packard.
+ */
+
+static const struct hash_table_arrangement hash_table_arrangements [] = {
+    { 16,		43,		41		},
+    { 32,		73,		71		},
+    { 64,		151,		149		},
+    { 128,		283,		281		},
+    { 256,		571,		569		},
+    { 512,		1153,		1151		},
+    { 1024,		2269,		2267		},
+    { 2048,		4519,		4517		},
+    { 4096,		9013,		9011		},
+    { 8192,		18043,		18041		},
+    { 16384,		36109,		36107		},
+    { 32768,		72091,		72089		},
+    { 65536,		144409,		144407		},
+    { 131072,		288361,		288359		},
+    { 262144,		576883,		576881		},
+    { 524288,		1153459,	1153457		},
+    { 1048576,		2307163,	2307161		},
+    { 2097152,		4613893,	4613891		},
+    { 4194304,		9227641,	9227639		},
+    { 8388608,		18455029,	18455027	},
+    { 16777216,		36911011,	36911009	},
+    { 33554432,		73819861,	73819859	},
+    { 67108864,		147639589,	147639587	},
+    { 134217728,	295279081,	295279079	},
+    { 268435456,	590559793,	590559791	}
+};
+
+#define NUM_HASH_TABLE_ARRANGEMENTS ARRAY_LENGTH (hash_table_arrangements)
+
+void hash_table_init(struct script *ctx,
+		     struct hash_table *ht,
+		     hash_keys_equal_func_t keys_equal)
+{
+	ht->ctx = ctx;
+	ht->keys_equal = keys_equal;
+
+	ht->arrangement = &hash_table_arrangements[0];
+
+	ht->entries = script_alloc0(ctx,
+				    ht->arrangement->size,
+				    sizeof(struct hash_entry *));
+
+	ht->live_entries = 0;
+	ht->used_entries = 0;
+	ht->iterating = 0;
+}
+
+void hash_table_fini(struct hash_table *ht)
+{
+	script_free(ht->ctx, ht->entries);
+}
+
+static struct hash_entry **
+hash_table_lookup_unique_key(struct hash_table *ht, struct hash_entry *key)
+{
+	unsigned long table_size, i, idx, step;
+	struct hash_entry **entry;
+
+	table_size = ht->arrangement->size;
+	idx = key->hash % table_size;
+
+	entry = &ht->entries[idx];
+	if (!ENTRY_IS_LIVE(*entry))
+		return entry;
+
+	i = 1;
+	step = key->hash % ht->arrangement->rehash;
+	if (step == 0)
+		step = 1;
+	do {
+		idx += step;
+		if (idx >= table_size)
+			idx -= table_size;
+
+		entry = &ht->entries[idx];
+		if (!ENTRY_IS_LIVE(*entry))
+			return entry;
+	} while (++i < table_size);
+
+	return NULL;
+}
+
+static void hash_table_manage(struct hash_table *ht)
+{
+	struct hash_table tmp;
+	bool realloc = true;
+	unsigned long i;
+
+	/* This keeps the size of the hash table between 2 and approximately 8
+	 * times the number of live entries and keeps the proportion of free
+	 * entries (search-terminations) > 25%.
+	 */
+	unsigned long high = ht->arrangement->high_water_mark;
+	unsigned long low = high >> 2;
+	unsigned long max_used = high  + high / 2;
+
+	tmp = *ht;
+
+	if (ht->live_entries > high) {
+		tmp.arrangement = ht->arrangement + 1;
+		/* This code is being abused if we can't make a table big enough. */
+	} else if (ht->live_entries < low &&
+		   /* Can't shrink if we're at the smallest size */
+		   ht->arrangement != &hash_table_arrangements[0]) {
+		tmp.arrangement = ht->arrangement - 1;
+	} else if (ht->used_entries > max_used) {
+		/* Clean out dead entries to prevent lookups from becoming too slow. */
+		for (i = 0; i < ht->arrangement->size; ++i) {
+			if (ENTRY_IS_DEAD (ht->entries[i]))
+				ht->entries[i] = NULL;
+		}
+		ht->used_entries = ht->live_entries;
+
+		/* There is no need to reallocate but some entries may need to be
+		 * moved.  Typically the proportion of entries needing to be moved is
+		 * small, but, if the moving should leave a large number of dead
+		 * entries, they will be cleaned out next time this code is
+		 * executed.
+		 */
+		realloc = false;
+	} else {
+		return;
+	}
+
+	if (realloc) {
+		tmp.entries = script_alloc0 (ht->ctx,
+					     tmp.arrangement->size, 
+					     sizeof(struct hash_entry *));
+		ht->used_entries = 0;
+	}
+
+	for (i = 0; i < ht->arrangement->size; ++i) {
+		struct hash_entry *entry, **pos;
+
+		entry = ht->entries[i];
+		if (ENTRY_IS_LIVE (entry)) {
+			ht->entries[i] = DEAD_ENTRY;
+
+			pos = hash_table_lookup_unique_key(&tmp, entry);
+			if (ENTRY_IS_FREE (*pos))
+				ht->used_entries++;
+
+			*pos = entry;
+		}
+	}
+
+	if (realloc) {
+		free (ht->entries);
+		ht->entries = tmp.entries;
+		ht->arrangement = tmp.arrangement;
+	}
+}
+
+static bool keys_equal(struct hash_table *ht,
+		       const struct hash_entry *key,
+		       const struct hash_entry *entry)
+{
+	if (entry->hash != key->hash)
+		return false;
+
+	return ht->keys_equal(key, entry);
+}
+
+void *hash_table_lookup(struct hash_table *ht, const struct hash_entry *key)
+{
+	struct hash_entry **entry;
+	unsigned long table_size, i, idx, step;
+
+	table_size = ht->arrangement->size;
+	idx = key->hash % table_size;
+	entry = &ht->entries[idx];
+
+	if (ENTRY_IS_LIVE (*entry)) {
+		if (keys_equal(ht, key, *entry))
+			return *entry;
+	} else if (ENTRY_IS_FREE (*entry))
+		return NULL;
+
+	i = 1;
+	step = key->hash % ht->arrangement->rehash;
+	if (step == 0)
+		step = 1;
+	do {
+		idx += step;
+		if (idx >= table_size)
+			idx -= table_size;
+
+		entry = &ht->entries[idx];
+		if (ENTRY_IS_LIVE(*entry)) {
+			if (keys_equal(ht, key, *entry))
+				return *entry;
+		} else if (ENTRY_IS_FREE(*entry))
+			return NULL;
+	} while (++i < table_size);
+
+	return NULL;
+}
+
+void *hash_table_lookup_unique(struct hash_table *ht, unsigned long id)
+{
+	struct hash_entry **entry;
+	unsigned long table_size, i, idx, step;
+
+	table_size = ht->arrangement->size;
+	idx = id % table_size;
+	entry = &ht->entries[idx];
+
+	if (ENTRY_IS_LIVE(*entry)) {
+		if ((*entry)->hash == id)
+			return *entry;
+	} else if (ENTRY_IS_FREE(*entry))
+		return NULL;
+
+	i = 1;
+	step = id % ht->arrangement->rehash;
+	if (step == 0)
+		step = 1;
+	do {
+		idx += step;
+		if (idx >= table_size)
+			idx -= table_size;
+
+		entry = &ht->entries[idx];
+		if (ENTRY_IS_LIVE(*entry)) {
+			if ((*entry)->hash == id)
+				return *entry;
+		} else if (ENTRY_IS_FREE(*entry))
+			return NULL;
+	} while (++i < table_size);
+
+	return NULL;
+}
+
+void hash_table_insert(struct hash_table *ht, struct hash_entry *key_and_value)
+{
+	struct hash_entry **entry;
+
+	ht->live_entries++;
+	hash_table_manage (ht);
+
+	entry = hash_table_lookup_unique_key(ht, key_and_value);
+	if (ENTRY_IS_FREE(*entry))
+		ht->used_entries++;
+
+	*entry = key_and_value;
+}
+
+static struct hash_entry **
+hash_table_lookup_exact_key(struct hash_table *ht, struct hash_entry *key)
+{
+	unsigned long table_size, i, idx, step;
+	struct hash_entry **entry;
+
+	table_size = ht->arrangement->size;
+	idx = key->hash % table_size;
+
+	entry = &ht->entries[idx];
+	if (*entry == key)
+		return entry;
+
+	i = 1;
+	step = key->hash % ht->arrangement->rehash;
+	if (step == 0)
+		step = 1;
+	do {
+		idx += step;
+		if (idx >= table_size)
+			idx -= table_size;
+
+		entry = &ht->entries[idx];
+		if (*entry == key)
+			return entry;
+	} while (++i < table_size);
+
+	return NULL;
+}
+
+void hash_table_remove(struct hash_table *ht, struct hash_entry *key)
+{
+	*hash_table_lookup_exact_key(ht, key) = DEAD_ENTRY;
+	ht->live_entries--;
+
+	/* Check for table resize. Don't do this when iterating as this will
+	 * reorder elements of the table and cause the iteration to potentially
+	 * skip some elements.
+	 */
+	if (ht->iterating == 0) {
+		/* This call _can_ fail, but only in failing to allocate new
+		 * memory to shrink the hash table. It does leave the table in a
+		 * consistent state, and we've already succeeded in removing the
+		 * entry, so we don't examine the failure status of this call.
+		 */
+		hash_table_manage(ht);
+	}
+}
+
+void hash_table_foreach(struct hash_table *ht,
+			hash_callback_func_t  hash_callback,
+			void *closure)
+{
+	unsigned long i;
+	struct hash_entry *entry;
+
+	/* Mark the table for iteration */
+	++ht->iterating;
+	for (i = 0; i < ht->arrangement->size; i++) {
+		entry = ht->entries[i];
+		if (ENTRY_IS_LIVE(entry))
+			hash_callback (entry, closure);
+	}
+	/* If some elements were deleted during the iteration,
+	 * the table may need resizing. Just do this every time
+	 * as the check is inexpensive.
+	 */
+	if (--ht->iterating == 0) {
+		/* Should we fail to shrink the hash table, it is left
+		 * unaltered, and we don't need to propagate the error status.
+		 */
+		hash_table_manage (ht);
+	}
+}
diff --git a/fuzz/interpreter.c b/fuzz/interpreter.c
new file mode 100644
index 0000000..b1c0ba0
--- /dev/null
+++ b/fuzz/interpreter.c
@@ -0,0 +1,362 @@
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "script.h"
+
+#ifndef MAX
+#define MAX(a,b) (((a)>=(b))?(a):(b))
+#endif
+
+struct perm_chunk {
+	struct perm_chunk *next;
+	long rem;
+	char *ptr;
+};
+
+void *script_alloc(struct script *ctx, long count, long size)
+{
+	void *ptr;
+
+	if (count > INT_MAX / size)
+		longjmp(ctx->error, -ENOMEM);
+
+	ptr =  malloc(count * size);
+	if (!ptr)
+		longjmp(ctx->error, -ENOMEM);
+
+	return ptr;
+}
+
+void *script_alloc0(struct script *ctx, long count, long size)
+{
+	return memset(script_alloc(ctx, count, size), 0, count*size);
+}
+
+void *script_realloc(struct script *ctx, void *ptr, long count, long size)
+{
+	if (count > INT_MAX / size)
+		longjmp(ctx->error, -ENOMEM);
+
+	ptr = realloc(ptr, count * size);
+	if (!ptr)
+		longjmp(ctx->error, -ENOMEM);
+
+	return ptr;
+}
+
+void script_free(struct script *ctx, void *ptr)
+{
+	free (ptr);
+}
+
+void *script_perm_alloc(struct script *ctx, int size)
+{
+	struct perm_chunk *chunk;
+	void *ptr;
+
+	size = (size + sizeof (void *)-1) & -sizeof (void *);
+
+	chunk = ctx->perm_chunk;
+	if (chunk == NULL || chunk->rem < size) {
+		int chunk_size = (size + 8191) & -8192;
+		chunk = script_alloc (ctx, 1, sizeof (struct perm_chunk) + chunk_size);
+		chunk->rem = chunk_size;
+		chunk->ptr = (char *) (chunk + 1);
+		chunk->next = ctx->perm_chunk;
+		ctx->perm_chunk = chunk;
+	}
+
+	ptr = chunk->ptr;
+	chunk->ptr += size;
+	chunk->rem -= size;
+
+	return ptr;
+}
+
+void *
+script_slab_alloc (struct script *ctx, int size)
+{
+#if DEBUG_MALLOC
+	return malloc (size);
+#else
+	int chunk_size;
+	struct perm_chunk *chunk;
+	void *ptr;
+
+	chunk_size = 2 * sizeof (void *);
+	chunk_size = (size + chunk_size - 1) / chunk_size;
+
+	if (ctx->slabs[chunk_size].free_list) {
+		ptr = ctx->slabs[chunk_size].free_list;
+		ctx->slabs[chunk_size].free_list = *(void **) ptr;
+		return ptr;
+	}
+
+	chunk = ctx->slabs[chunk_size].chunk;
+	if (chunk == NULL || ! chunk->rem) {
+		int cnt = MAX (128, 8192 / (chunk_size * 2 * sizeof (void *)));
+
+		chunk = script_alloc (ctx,
+				      1, sizeof (struct perm_chunk) +
+				      cnt * chunk_size * 2 * sizeof (void *));
+		chunk->rem = cnt;
+		chunk->ptr = (char *) (chunk + 1);
+		chunk->next = ctx->slabs[chunk_size].chunk;
+		ctx->slabs[chunk_size].chunk = chunk;
+	}
+
+	ptr = chunk->ptr;
+	chunk->ptr += chunk_size * 2 * sizeof (void *);
+	chunk->rem--;
+
+	return ptr;
+#endif
+}
+
+void
+script_slab_free (struct script *ctx, void *ptr, int size)
+{
+	int chunk_size;
+	void **free_list;
+
+	if (!ptr)
+		return;
+
+#if DEBUG_MALLOC
+	free (ptr);
+#else
+	chunk_size = 2 * sizeof (void *);
+	chunk_size = (size + chunk_size - 1) / chunk_size;
+
+	free_list = ptr;
+	*free_list = ctx->slabs[chunk_size].free_list;
+	ctx->slabs[chunk_size].free_list = ptr;
+#endif
+}
+
+static void
+perm_fini (struct script *ctx)
+{
+	while (ctx->perm_chunk != NULL) {
+		struct perm_chunk *chunk = ctx->perm_chunk;
+		ctx->perm_chunk = chunk->next;
+		script_free (ctx, chunk);
+	}
+}
+
+static void
+slab_fini (struct script *ctx)
+{
+	unsigned int i;
+
+	for (i = 0; i < sizeof (ctx->slabs) / sizeof (ctx->slabs[0]); i++) {
+		while (ctx->slabs[i].chunk != NULL) {
+			struct perm_chunk *chunk = ctx->slabs[i].chunk;
+			ctx->slabs[i].chunk = chunk->next;
+			script_free (ctx, chunk);
+		}
+	}
+}
+
+static void init_dictionaries(struct script *ctx)
+{
+	struct stack *stack = &ctx->dstack;
+
+	stack_init(ctx, stack, 4);
+	stack_push(stack, dictionary_new(ctx)); /* systemdict */
+	stack_push(stack, dictionary_new(ctx)); /* extensiondict */
+	stack_push(stack, dictionary_new(ctx)); /* userdict */
+
+	script_init_operators(ctx);
+	script_init_constants(ctx);
+}
+
+struct dictionary *systemdict(struct script *ctx)
+{
+	return ctx->dstack.objects[0].dictionary;
+}
+
+void systemdict_execute(struct script *ctx, const char *key)
+{
+	struct dictionary *dict = ctx->dstack.objects[0].dictionary;
+	name_t name = name_new_static(ctx, key).name;
+	struct dictionary_entry *entry;
+
+	entry = hash_table_lookup_unique(&dict->ht, name);
+	if (!entry)
+		longjmp(ctx->error, -EINVAL);
+
+	return obj_execute(ctx, entry->obj);
+}
+
+struct dictionary *extensiondict(struct script *ctx)
+{
+	return ctx->dstack.objects[1].dictionary;
+}
+
+
+/* intern string */
+
+struct intern_string {
+	struct hash_entry hash_entry;
+	int len;
+	const char *string;
+};
+
+static unsigned long
+_intern_string_hash(const char *str, int len)
+{
+	const signed char *p = (const signed char *) str;
+	if (len > 0) {
+		unsigned int h = *p;
+
+		while (--len)
+			h = (h << 5) - h + *++p;
+
+		return h;
+	}
+	return 0;
+}
+
+static bool _intern_string_equal(const void *_a, const void *_b)
+{
+	const struct intern_string *a = _a;
+	const struct intern_string *b = _b;
+
+	if (a->len != b->len)
+		return false;
+
+	return memcmp(a->string, b->string, a->len) == 0;
+}
+
+int script_init(struct script *ctx)
+{
+	int ret;
+
+	memset(ctx, 0, sizeof (*ctx));
+
+	ret = setjmp(ctx->error);
+	if (ret)
+		return ret;
+
+	hash_table_init(ctx, &ctx->strings, _intern_string_equal);
+	stack_init(ctx, &ctx->ostack, 2048);
+	init_dictionaries(ctx);
+
+	scanner_init(ctx, &ctx->scanner);
+
+	return 0;
+}
+
+static void
+intern_print(void *entry, void *data)
+{
+	struct intern_string *string = entry;
+	fprintf(data, "word_%s=\"%s\"\n", string->string, string->string);
+}
+
+void script_print_strings(struct script *ctx, FILE *file)
+{
+	hash_table_foreach(&ctx->strings, intern_print, file);
+}
+
+void name_define(struct script *ctx, name_t name, obj_t obj)
+{
+	dictionary_set(ctx->dstack.objects[ctx->dstack.len-1].dictionary,
+		       name, obj);
+}
+
+obj_t name_lookup(struct script *ctx, name_t name)
+{
+	int i;
+
+	for (i = ctx->dstack.len; i--; ) {
+		struct dictionary *dict = ctx->dstack.objects[i].dictionary;
+		struct dictionary_entry *entry;
+
+		entry = hash_table_lookup_unique(&dict->ht, name);
+		if (entry)
+			return entry->obj;
+	}
+
+	longjmp(ctx->error, -EINVAL);
+}
+
+void name_undefine(struct script *ctx, name_t name)
+{
+	long i;
+
+	for (i = ctx->dstack.len; --i; ) {
+		struct dictionary *dict = ctx->dstack.objects[i].dictionary;
+		if (dictionary_has(dict, name)) {
+			dictionary_unset(dict, name);
+			return;
+		}
+	}
+
+	longjmp(ctx->error, -EINVAL);
+}
+
+void *intern_string(struct script *ctx, const char *str, int len)
+{
+	struct intern_string tmpl, *atom;
+
+	tmpl.hash_entry.hash = _intern_string_hash(str, len);
+	tmpl.len = len;
+	tmpl.string = str;
+
+	atom = hash_table_lookup(&ctx->strings, &tmpl.hash_entry);
+	if (atom == NULL) {
+		atom = script_perm_alloc(ctx, sizeof(struct intern_string) + len + 1);
+		atom->hash_entry.hash = tmpl.hash_entry.hash;
+		atom->len = tmpl.len;
+		atom->string = (char *)(atom + 1);
+		memcpy(atom + 1, str, len);
+		((char *)(atom + 1))[len] = '\0';
+
+		hash_table_insert(&ctx->strings, &atom->hash_entry);
+	}
+
+	return atom+1;
+}
+
+int script_run(struct script *ctx, const char *filename)
+{
+	obj_t file;
+	int ret;
+
+	if (filename == NULL)
+		file = file_new_for_stream(ctx, stdin);
+	else
+		file = file_new(ctx, filename, "r");
+	file.type |= OBJ_ATTR_EXECUTABLE;
+
+	ret = setjmp(ctx->error);
+	if (ret == 0)
+		file_execute(file.file);
+	obj_free(file);
+
+	return ret;
+}
+
+void script_fini(struct script *ctx)
+{
+	stack_fini(&ctx->ostack);
+	stack_fini(&ctx->dstack);
+	scanner_fini(&ctx->scanner);
+
+	hash_table_fini(&ctx->strings);
+	if (ctx->free_array)
+		array_free(ctx->free_array);
+	if (ctx->free_dictionary)
+		dictionary_free(ctx->free_dictionary);
+	if (ctx->free_string)
+		string_free(ctx->free_string);
+
+	slab_fini(ctx);
+	perm_fini(ctx);
+}
diff --git a/fuzz/objects.c b/fuzz/objects.c
new file mode 100644
index 0000000..7438f93
--- /dev/null
+++ b/fuzz/objects.c
@@ -0,0 +1,467 @@
+#include <string.h>
+
+#include "script.h"
+
+static inline void __obj_execute(struct script *ctx, obj_t obj);
+
+obj_t array_new(struct script *ctx, long initial_size)
+{
+	struct array *array;
+
+	if (!ctx->free_array || ctx->free_array->stack.size <= initial_size) {
+		array = script_slab_alloc(ctx, sizeof (struct array));
+		stack_init (ctx, &array->stack, initial_size ? initial_size : 32);
+	} else {
+		array = ctx->free_array;
+		ctx->free_array = NULL;
+	}
+
+	return new_heap_object(&array->base, ctx, OBJ_TYPE_ARRAY);
+}
+
+void array_set(struct array *array, long elem, obj_t value)
+{
+	if (elem < 0)
+		longjmp(array->base.ctx->error, -EINVAL);
+
+	if (elem >= array->stack.len) {
+		stack_grow(&array->stack, elem + 1);
+
+		memset(array->stack.objects + array->stack.len,
+		       0, (elem - array->stack.len + 1) * sizeof (obj_t));
+		array->stack.len = elem + 1;
+	}
+
+	obj_free(array->stack.objects[elem]);
+	array->stack.objects[elem] = obj_reference(value);
+}
+
+obj_t array_get(struct array *array, long elem)
+{
+	if (elem < 0 || elem > array->stack.len)
+		longjmp(array->base.ctx->error, -EINVAL);
+
+	return array->stack.objects[elem];
+}
+
+void array_append(struct array *array, obj_t obj)
+{
+	stack_push(&array->stack, obj_reference(obj));
+}
+
+inline void array_execute(struct array *array)
+{
+	long i;
+
+	if (!array->stack.len)
+		return;
+
+	for (i = 0; i < array->stack.len; i++) {
+		obj_t obj = array->stack.objects[i];
+
+		if ((obj.type & (OBJ_TYPE_ARRAY | OBJ_ATTR_EXECUTABLE)) == OBJ_ATTR_EXECUTABLE) {
+			__obj_execute(array->base.ctx, obj);
+			continue;
+		}
+
+		ostack_push_copy(array->base.ctx, obj);
+	}
+}
+
+void array_free(struct array *array)
+{
+#if DEBUG_MALLOC
+	stack_fini (array->base.ctx, &array->stack);
+	script_slab_free(array->base.ctx, array, sizeof (struct array));
+#else
+	long n;
+
+	for (n = 0; n < array->stack.len; n++)
+		obj_free (array->stack.objects[n]);
+	array->stack.len = 0;
+
+	if (array->base.ctx->free_array) {
+		if (array->stack.size > array->base.ctx->free_array->stack.size) {
+			struct array *tmp = array->base.ctx->free_array;
+			array->base.ctx->free_array = array;
+			array = tmp;
+		}
+
+		stack_fini(&array->stack);
+		script_slab_free(array->base.ctx, array, sizeof (struct array));
+	} else
+		array->base.ctx->free_array = array;
+#endif
+}
+
+obj_t dictionary_new(struct script *ctx)
+{
+	struct dictionary *dict;
+
+	if (ctx->free_dictionary) {
+		dict = ctx->free_dictionary;
+		ctx->free_dictionary = NULL;
+	} else {
+		dict = script_slab_alloc(ctx, sizeof(struct dictionary));
+		hash_table_init(ctx, &dict->ht, NULL);
+	}
+
+	return new_heap_object(&dict->base, ctx, OBJ_TYPE_DICTIONARY);
+}
+
+struct _dictionary_entry_pluck {
+	struct script *ctx;
+	struct hash_table *ht;
+};
+
+static void
+_dictionary_entry_pluck (void *entry, void *data)
+{
+	struct dictionary_entry *dict_entry;
+	struct _dictionary_entry_pluck *pluck_data;
+
+	dict_entry = entry;
+	pluck_data = data;
+
+	hash_table_remove(pluck_data->ht, entry);
+	obj_free(dict_entry->obj);
+	script_slab_free(pluck_data->ctx,
+			 entry, sizeof(struct dictionary_entry));
+}
+
+void
+dictionary_free(struct dictionary *dict)
+{
+	struct _dictionary_entry_pluck data;
+
+	data.ctx = dict->base.ctx;
+	data.ht = &dict->ht;
+	hash_table_foreach (&dict->ht, _dictionary_entry_pluck, &data);
+
+#if DEBUG_MALLOC
+	hash_table_fini(&dict->ht);
+	script_slab_free(ctx, dict, sizeof(struct dictionary));
+#else
+	if (dict->base.ctx->free_dictionary) {
+		hash_table_fini(&dict->ht);
+		script_slab_free(dict->base.ctx,
+				 dict, sizeof(struct dictionary));
+	} else
+		dict->base.ctx->free_dictionary = dict;
+#endif
+}
+
+void dictionary_set(struct dictionary *dict, name_t name, obj_t value)
+{
+	struct dictionary_entry *entry;
+
+	entry = hash_table_lookup_unique(&dict->ht, name);
+	if (entry) {
+		/* replace the existing entry */
+		obj_free(entry->obj);
+		entry->obj = obj_reference(value);
+		return;
+	}
+
+	entry = script_slab_alloc(dict->base.ctx, sizeof(*entry));
+	entry->hash_entry.hash = name;
+	entry->obj = obj_reference(value);
+	hash_table_insert(&dict->ht, &entry->hash_entry);
+}
+
+obj_t dictionary_get(struct dictionary *dict, name_t name)
+{
+    struct dictionary_entry *entry;
+
+    entry = hash_table_lookup_unique(&dict->ht, name);
+    if (!entry)
+	    return null();
+
+    return entry->obj;
+}
+
+bool dictionary_has(struct dictionary *dict, name_t name)
+{
+	return hash_table_lookup_unique(&dict->ht, name);
+}
+
+void dictionary_unset(struct dictionary *dict, name_t name)
+{
+	struct dictionary_entry *entry;
+
+	entry = hash_table_lookup_unique(&dict->ht, name);
+	if (entry) {
+		hash_table_remove(&dict->ht, &entry->hash_entry);
+		obj_free(entry->obj);
+		script_slab_free(dict->base.ctx,
+				 entry, sizeof (struct dictionary_entry));
+	}
+}
+
+static obj_t new_name(const char *str)
+{
+	return (obj_t){.type = OBJ_TYPE_NAME, .name = (name_t)str };
+}
+
+obj_t name_new(struct script *ctx, const char *str, int len)
+{
+	return new_name(intern_string(ctx, str, len));
+}
+
+obj_t name_new_static(struct script *ctx, const char *str)
+{
+	return new_name(intern_string(ctx, str, strlen(str)));
+}
+
+static inline obj_t new_string(struct string *string)
+{
+	return (obj_t){.type = OBJ_TYPE_STRING, .string = string};
+}
+
+obj_t string_new(struct script *ctx, const char *str, int len)
+{
+	struct string *string;
+
+	if (len < 0)
+		len = strlen (str);
+
+	if (ctx->free_string == NULL || ctx->free_string->len <= len) {
+		string = script_slab_alloc(ctx, sizeof(struct string));
+		string->string = script_alloc(ctx, 1, len + 1);
+	} else {
+		string = ctx->free_string;
+		ctx->free_string = NULL;
+	}
+
+	if (str) {
+		memcpy (string->string, str, len);
+		string->string[len] = '\0';
+	}
+	string->len = len;
+	string->deflate = 0;
+	string->method = NONE;
+
+	return new_heap_object(&string->base, ctx, OBJ_TYPE_STRING);
+}
+
+obj_t
+string_deflate_new(struct script *ctx, void *bytes, int in_len, int out_len)
+{
+	obj_t s = string_new(ctx, bytes, in_len);
+
+	s.string->deflate = out_len;
+	s.string->method = ZLIB;
+
+	return s;
+}
+
+obj_t string_new_from_bytes (struct script *ctx, char *bytes, unsigned int len)
+{
+	struct string *string;
+
+	string = script_slab_alloc(ctx, sizeof(struct string));
+	string->string = bytes;
+	string->len = len;
+	string->deflate = 0;
+	string->method = NONE;
+
+	return new_heap_object(&string->base, ctx, OBJ_TYPE_STRING);
+}
+
+static inline void string_execute(struct string *string)
+{
+	obj_t obj;
+
+	if (!string->len)
+		return;
+
+	obj = file_new_for_bytes(string->base.ctx, string->string, string->len);
+	scan_file(obj.file);
+	obj_free(obj);
+}
+
+void string_free(struct string *string)
+{
+#if DEBUG_MALLOC
+	script_free(ctx, string->string);
+	script_slab_free(ctx, string, sizeof(struct string));
+#else
+	if (string->base.ctx->free_string) {
+		if (string->len > string->base.ctx->free_string->len) {
+			struct string *tmp = string->base.ctx->free_string;
+			string->base.ctx->free_string = string;
+			string = tmp;
+		}
+
+		script_free(string->base.ctx, string->string);
+		script_slab_free(string->base.ctx,
+				 string, sizeof(struct string));
+	} else
+		string->base.ctx->free_string = string;
+#endif
+}
+
+static inline void __obj_execute(struct script *ctx, obj_t obj)
+{
+	do switch (obj.type & OBJ_TYPE_MASK) {
+	case OBJ_TYPE_OPERATOR:
+		return obj.op(ctx);
+
+	case OBJ_TYPE_ARRAY:
+		return array_execute(obj.array);
+	case OBJ_TYPE_FILE:
+		return file_execute(obj.file);
+	case OBJ_TYPE_STRING:
+		return string_execute(obj.string);
+
+	case OBJ_TYPE_NAME:
+		obj = name_lookup(ctx, obj.name);
+		if (obj.type & OBJ_ATTR_EXECUTABLE)
+			break;
+
+		/* fallthrough */
+	default:
+		return ostack_push_copy(ctx, obj);
+	} while (true);
+}
+
+void obj_execute(struct script *ctx, obj_t obj)
+{
+	__obj_execute(ctx, obj);
+}
+
+void obj_free(struct stack_object obj)
+{
+	if (!OBJ_IS_COMPOUND(obj))
+		return;
+
+	if (--obj.object->ref)
+		return;
+
+	switch (obj.type & OBJ_TYPE_MASK) {
+	case OBJ_TYPE_ARRAY:
+		array_free(obj.array);
+		break;
+	case OBJ_TYPE_DICTIONARY:
+		dictionary_free(obj.dictionary);
+		break;
+	case OBJ_TYPE_FILE:
+		file_free(obj.file);
+		break;
+	case OBJ_TYPE_STRING:
+		string_free(obj.string);
+		break;
+	default:
+		if ((obj.type & OBJ_TYPE_MASK) >= OBJ_TYPE_EXTENSION_0)
+			obj.extension->ops->free(obj.ptr);
+		break;
+	}
+}
+
+obj_t obj_as_file(struct script *ctx, obj_t src)
+{
+    int type = obj_type (src);
+    switch (type) {
+    case OBJ_TYPE_FILE:
+	return obj_reference (src);
+    case OBJ_TYPE_STRING:
+	 return file_new_from_string(ctx, src.string);
+    case OBJ_TYPE_ARRAY:
+#if 0
+	if (src->type & OBJ_ATTR_EXECUTABLE)
+	    return file_new_from_procedure (cs, src);
+#endif
+    default:
+	longjmp(ctx->error, -EINVAL);
+    }
+}
+
+static int
+lexcmp(void const *a, size_t alen,
+       void const *b, size_t blen)
+{
+    size_t len = alen < blen ? alen : blen;
+    int cmp = memcmp (a, b, len);
+    if (cmp)
+	return cmp;
+    if (alen == blen)
+	return 0;
+    return alen < blen ? -1 : +1;
+}
+
+int obj_compare(obj_t a, obj_t b)
+{
+	int atype = obj_type (a);
+	int btype = obj_type (b);
+	int sign;
+
+#define CMP(x,y) ((x) < (y) ? -1 : +1)
+
+	if (atype == btype) {
+		switch (atype) {
+		case OBJ_TYPE_BOOLEAN:
+			return CMP(a.boolean, b.boolean);
+		case OBJ_TYPE_INTEGER:
+			return CMP(a.integer, b.integer);
+		case OBJ_TYPE_REAL:
+			return CMP(a.real, b.real);
+		case OBJ_TYPE_NAME:
+			return CMP(a.name, b.name);
+		case OBJ_TYPE_STRING:
+			if (a.string == b.string)
+				return 0;
+
+			return lexcmp (a.string->string, a.string->len,
+				       b.string->string, b.string->len);
+		case OBJ_TYPE_OPERATOR:
+		case OBJ_TYPE_ARRAY:
+		case OBJ_TYPE_DICTIONARY:
+		case OBJ_TYPE_FILE:
+			return (intptr_t)a.ptr - (intptr_t)b.ptr;
+
+		case OBJ_TYPE_NULL:
+		case OBJ_TYPE_MARK:
+			goto TYPE_CHECK_ERROR;
+		}
+	}
+
+	sign = +1;
+	if (atype < btype) {
+		obj_t tmp;
+
+		tmp = a; a = b; b = tmp;
+
+		atype = obj_type (a);
+		btype = obj_type (b);
+		sign = -1;
+	}
+
+	switch ((int) atype) {
+	case OBJ_TYPE_INTEGER:
+		if (btype == OBJ_TYPE_BOOLEAN)
+			return sign * CMP (a.integer, !!b.boolean);
+		break;
+	case OBJ_TYPE_REAL:
+		if (btype == OBJ_TYPE_INTEGER)
+			return sign * CMP (a.real, b.integer);
+		else if (btype == OBJ_TYPE_BOOLEAN)
+			return sign * CMP (a.real, !!b.boolean);
+		break;
+
+	case OBJ_TYPE_STRING:
+		if (btype == OBJ_TYPE_NAME) {
+			const char *bstr = (const char *) b.name;
+			return sign * lexcmp(a.string->string, a.string->len,
+					     bstr, strlen (bstr));
+		}
+		break;
+
+	default:
+		break;
+	}
+
+#undef CMP
+
+TYPE_CHECK_ERROR:
+	return a.type - b.type;
+}
diff --git a/fuzz/operators.c b/fuzz/operators.c
new file mode 100644
index 0000000..6f867de
--- /dev/null
+++ b/fuzz/operators.c
@@ -0,0 +1,1161 @@
+#include <stdio.h> /* snprintf */
+#include <stdlib.h> /* mkstemp */
+#include <string.h>
+
+#include <math.h>
+#include <limits.h> /* INT_MAX */
+#include <assert.h>
+
+#include "igt.h"
+#include "script.h"
+
+#define check(CNT) ostack_check(ctx, CNT)
+#define pop(CNT) ostack_pop(ctx, (CNT))
+#define push(OBJ) ostack_push(ctx, (OBJ))
+
+#define ERR(expr) do if ((expr)) longjmp(ctx->error, -EINVAL); while(0)
+
+static bool ostack_get_boolean(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+	switch (obj_type (obj)) {
+	case OBJ_TYPE_BOOLEAN:
+		return obj.boolean;
+	case OBJ_TYPE_INTEGER:
+		return !!obj.integer;
+	case OBJ_TYPE_REAL:
+		return obj.real != 0.;
+	default:
+		ERR(1);
+	}
+}
+
+static long ostack_get_integer(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+	switch (obj_type(obj)) {
+	case OBJ_TYPE_BOOLEAN:
+		return obj.boolean;
+	case OBJ_TYPE_INTEGER:
+		return obj.integer;
+	case OBJ_TYPE_REAL:
+		return obj.real;
+	default:
+		ERR(1);
+	}
+}
+
+static name_t ostack_get_name(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get (ctx, i);
+
+	ERR(obj_type(obj) != OBJ_TYPE_NAME);
+
+	return obj.name;
+}
+
+static struct array *ostack_get_procedure(struct script *ctx, unsigned int i)
+{
+	obj_t obj = ostack_get(ctx, i);
+
+	ERR(!obj_is_procedure(obj));
+
+	obj.array->base.ref++;
+	return obj.array;
+}
+
+static void end_dict_construction(struct script *ctx)
+{
+	obj_t obj;
+	struct dictionary *dict;
+
+	obj = dictionary_new(ctx);
+
+	dict = obj.dictionary;
+	do {
+		obj_t name, value;
+
+		check(1);
+
+		value = ostack_get (ctx, 0);
+		if (obj_type (value) == OBJ_TYPE_MARK) {
+			pop(1);
+			break;
+		}
+
+		check(2);
+
+		name = ostack_get (ctx, 1);
+		ERR(obj_type (name) != OBJ_TYPE_NAME);
+
+		dictionary_set(dict, name.name, value);
+
+		pop(2);
+	} while (true);
+
+	push (obj);
+}
+
+static void end_array_construction(struct script *ctx)
+{
+	obj_t obj;
+	int len = 0;
+
+	do {
+		check(len + 1);
+
+		if (obj_type (ostack_get (ctx, len)) == OBJ_TYPE_MARK)
+			break;
+
+		len++;
+	} while (true);
+
+	obj = array_new (ctx, len);
+	if (len != 0) {
+		struct array *array = obj.array;
+		memcpy (array->stack.objects,
+			ostack_inplace(ctx, len - 1),
+			sizeof(obj_t) * len);
+		array->stack.len = len;
+	}
+	ctx->ostack.len -= len + 1;
+
+	push(obj);
+}
+
+static void _add(struct script *ctx)
+{
+	obj_t A, B;
+	enum object_type type_a, type_b;
+
+	check (2);
+
+	B = ostack_get (ctx, 0);
+	A = ostack_get (ctx, 1);
+
+	type_a = obj_type (A);
+	ERR(!(type_a == OBJ_TYPE_INTEGER || type_a == OBJ_TYPE_REAL));
+
+	type_b = obj_type (B);
+	ERR(!(type_b == OBJ_TYPE_INTEGER || type_b == OBJ_TYPE_REAL));
+
+	pop (2);
+
+	if (type_a == OBJ_TYPE_REAL && type_b == OBJ_TYPE_REAL)
+		ostack_push_real (ctx, A.real + B.real);
+	else if (type_a == OBJ_TYPE_INTEGER && type_b == OBJ_TYPE_INTEGER)
+		ostack_push_integer (ctx, A.integer + B.integer);
+	else {
+		double v;
+
+		if (type_a == OBJ_TYPE_REAL)
+			v = A.real;
+		else
+			v = A.integer;
+
+		if (type_b == OBJ_TYPE_REAL)
+			v += B.real;
+		else
+			v += B.integer;
+
+		ostack_push_real (ctx, v);
+	}
+}
+
+static void _and(struct script *ctx)
+{
+	obj_t a, b;
+
+	check (2);
+
+	a = ostack_get (ctx, 0);
+	b = ostack_get (ctx, 1);
+	ERR (obj_type (a) != obj_type (b));
+
+	pop (2);
+	switch (obj_type (a)) {
+	case OBJ_TYPE_INTEGER:
+		ostack_push_integer (ctx, a.integer & b.integer);
+	case OBJ_TYPE_BOOLEAN:
+		ostack_push_boolean (ctx, a.boolean & b.boolean);
+	default:
+		ERR(1);
+	}
+}
+
+static void _array(struct script *ctx)
+{
+	return push(array_new(ctx, 0));
+}
+
+static void _bind_substitute(struct script *ctx, struct array *array)
+{
+	long n;
+
+	/* perform operator substitution on the executable array (procedure) */
+	n = array->stack.len;
+	for (long i = 0; i < n; i++) {
+		obj_t obj = array->stack.objects[i];
+
+		if (obj.type == (OBJ_TYPE_NAME | OBJ_ATTR_EXECUTABLE)) {
+			for (long j = ctx->dstack.len; j--; ) {
+				struct dictionary *dict = ctx->dstack.objects[j].dictionary;
+				struct dictionary_entry *entry;
+
+				entry = hash_table_lookup_unique(&dict->ht,
+								 obj.name);
+				if (entry) {
+					array->stack.objects[i] = entry->obj;
+					break;
+				}
+			}
+		} else if (obj_is_procedure(obj))
+			_bind_substitute(ctx, obj.array);
+	}
+}
+
+static void _idiom_substitute(struct script *ctx, struct array *array)
+{
+#if 0
+	long i, j;
+
+	/* XXX substring search, build array once then search for
+	 * longest matching idiom, repeat. */
+
+	/* scan the top-most array for sequences we can pre-compile */
+
+	/* now recurse for subroutines */
+	j = array->stack.len;
+	for (i = 0; i < j; i++) {
+		obj_t *obj = &array->stack.objects[i];
+
+		if (obj_is_procedure (obj)) {
+			_idiom_substitute (ctx, obj.array);
+		}
+	}
+#endif
+}
+
+static void _bind(struct script *ctx)
+{
+	struct array *array;
+
+	check (1);
+
+	array = ostack_get_procedure (ctx, 0);
+	_bind_substitute (ctx, array);
+	_idiom_substitute (ctx, array);
+	array->base.ref--;
+}
+
+static void _bitshift(struct script *ctx)
+{
+	long v, shift;
+
+	check (2);
+
+	shift = ostack_get_integer(ctx, 0);
+	v = ostack_get_integer(ctx, 1);
+
+	if (shift < 0) {
+		shift = -shift;
+		v >>= shift;
+	} else
+		v <<= shift;
+
+	pop (1);
+	ostack_inplace(ctx, 0)->integer = v;
+}
+
+static void _copy (struct script *ctx)
+{
+	obj_t obj;
+
+	check (1);
+
+	obj = obj_reference (ostack_get (ctx, 0));
+	pop (1);
+
+	switch (obj_type (obj)) {
+		/*XXX array, string, dictionary, etc */
+	case OBJ_TYPE_INTEGER:
+		{
+			long i, n;
+
+			n = obj.integer;
+			ERR(n < 0);
+			check (n);
+
+			for (i = n; i--; )
+				ostack_push_copy (ctx, ostack_get (ctx, n-1));
+			break;
+		}
+	default:
+		ERR(1);
+	}
+}
+
+static void _cvi(struct script *ctx)
+{
+	obj_t val, obj;
+
+	check (1);
+
+	val = ostack_get (ctx, 0);
+	switch (obj_type (val)) {
+	case OBJ_TYPE_INTEGER:
+		break;
+
+	case OBJ_TYPE_REAL:
+		pop (1);
+		ostack_push_integer(ctx, val.real);
+		break;
+
+	case OBJ_TYPE_STRING:
+		ERR(!parse_number(&obj, val.string->string, val.string->len));
+
+		pop (1);
+		if (obj_type (obj) == OBJ_TYPE_INTEGER)
+			push (obj);
+		else
+			ostack_push_integer (ctx, obj.real);
+		break;
+
+	default:
+		ERR(1);
+	}
+}
+
+static void _cvr(struct script *ctx)
+{
+	obj_t val, obj;
+
+	check (1);
+
+	val = ostack_get (ctx, 0);
+	switch (obj_type (val)) {
+	case OBJ_TYPE_REAL:
+		break;
+
+	case OBJ_TYPE_INTEGER:
+		pop (1);
+		ostack_push_real (ctx, val.integer);
+		break;
+
+	case OBJ_TYPE_STRING:
+		ERR(!parse_number(&obj, val.string->string, val.string->len));
+
+		pop (1);
+		if (obj_type (obj) == OBJ_TYPE_REAL)
+			push (obj);
+		else
+			ostack_push_real (ctx, obj.integer);
+		break;
+
+	default:
+		ERR(1);
+	}
+}
+
+static void _def(struct script *ctx)
+{
+	check (2);
+	name_define (ctx, ostack_get_name (ctx, 1), ostack_get (ctx, 0));
+	pop (2);
+}
+
+static void _dict(struct script *ctx)
+{
+	push(dictionary_new(ctx));
+}
+
+static void _div(struct script *ctx)
+{
+	obj_t A;
+	obj_t B;
+	enum object_type type_a, type_b;
+
+	check (2);
+
+	B = ostack_get (ctx, 0);
+	A = ostack_get (ctx, 1);
+
+	type_a = obj_type (A);
+	ERR(! (type_a == OBJ_TYPE_INTEGER || type_a == OBJ_TYPE_REAL));
+
+	type_b = obj_type (B);
+	ERR(!(type_b == OBJ_TYPE_INTEGER || type_b == OBJ_TYPE_REAL));
+
+	pop (2);
+
+	if (type_a == OBJ_TYPE_REAL && type_b == OBJ_TYPE_REAL)
+		ostack_push_real (ctx, A.real / B.real);
+	else if (type_a == OBJ_TYPE_INTEGER && type_b == OBJ_TYPE_INTEGER)
+		ostack_push_integer (ctx, A.integer / B.integer);
+	else {
+		double v;
+
+		if (type_a == OBJ_TYPE_REAL)
+			v = A.real;
+		else
+			v = A.integer;
+
+		if (type_b == OBJ_TYPE_REAL)
+			v /= B.real;
+		else
+			v /= B.integer;
+
+		ostack_push_real (ctx, v);
+	}
+}
+
+static void _duplicate (struct script *ctx)
+{
+	check (1);
+	ostack_push_copy (ctx, ostack_get (ctx, 0));
+}
+
+static void _eq(struct script *ctx)
+{
+	int cmp;
+
+	check (2);
+	cmp = obj_compare(ostack_get (ctx, 1), ostack_get (ctx, 0));
+	pop (2);
+
+	ostack_push_boolean(ctx, cmp == 0);
+}
+
+static void _exch(struct script *ctx)
+{
+	stack_exch(&ctx->ostack);
+}
+
+static void _false(struct script *ctx)
+{
+	ostack_push_boolean(ctx, false);
+}
+
+static void _for(struct script *ctx)
+{
+	struct array *proc;
+	long i, inc, limit;
+
+	check (4);
+	proc = ostack_get_procedure(ctx, 0);
+	limit = ostack_get_integer(ctx, 1);
+	inc = ostack_get_integer(ctx, 2);
+	i = ostack_get_integer(ctx, 3);
+	pop (4);
+
+	for (; i <= limit; i += inc) {
+		ostack_push_integer(ctx, i);
+		array_execute(proc);
+	}
+
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _foreach(struct script *ctx)
+{
+	struct array *proc;
+	obj_t obj;
+
+	check(2);
+	proc = ostack_get_procedure(ctx, 0);
+	obj = ostack_copy(ctx, 1);
+	pop(2);
+
+	if (obj_type(obj) == OBJ_TYPE_ARRAY) {
+		for (long n = 0; n < obj.array->stack.len; n++) {
+			ostack_push_copy(ctx, obj.array->stack.objects[n]);
+			array_execute(proc);
+		}
+	}
+
+	obj_free(obj);
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _fork(struct script *ctx)
+{
+	struct array *proc;
+	long count;
+
+	check(2);
+	proc = ostack_get_procedure(ctx, 0);
+	count = number_get_value(ostack_get(ctx, 1));
+	if (count < 0)
+		count = sysconf(_SC_NPROCESSORS_ONLN);
+	pop(2);
+
+	igt_fork(child, count) {
+		int ret = setjmp(ctx->error);
+		if (ret)
+			exit(-ret);
+
+		ostack_push_integer(ctx, child);
+		array_execute(proc);
+	}
+
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _ge(struct script *ctx)
+{
+	int cmp;
+
+	check (2);
+	cmp = obj_compare (ostack_get (ctx, 1), ostack_get (ctx, 0));
+	pop (2);
+
+	ostack_push_boolean (ctx, cmp >= 0);
+}
+
+static void _get(struct script *ctx)
+{
+	obj_t key, src, obj;
+
+	check (2);
+	key = ostack_get (ctx, 0);
+	src = ostack_get (ctx, 1);
+	pop (1);
+
+	switch (obj_type(src)) {
+	case OBJ_TYPE_DICTIONARY:
+		ERR(obj_type (key) != OBJ_TYPE_NAME);
+		obj = dictionary_get (src.dictionary, key.name);
+		break;
+
+	case OBJ_TYPE_ARRAY:
+		ERR(obj_type (key) != OBJ_TYPE_INTEGER);
+		obj = array_get (src.array, key.integer);
+		break;
+#if 0
+	case OBJ_TYPE_STRING:
+		obj = string_get (src, key);
+		break;
+#endif
+
+	default:
+		ERR(1);
+	}
+
+	ostack_push_copy(ctx, obj);
+}
+
+static void _gt (struct script *ctx)
+{
+	int cmp;
+
+	check (2);
+	cmp = obj_compare(ostack_get(ctx, 1), ostack_get(ctx, 0));
+	pop (2);
+
+	ostack_push_boolean (ctx, cmp > 0);
+}
+
+static void _if(struct script *ctx)
+{
+	struct array *proc;
+	bool predicate;
+
+	check(2);
+	proc = ostack_get_procedure(ctx, 0);
+	predicate = ostack_get_boolean(ctx, 1);
+	pop(2);
+
+	if (predicate)
+		array_execute(proc);
+
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _ifelse (struct script *ctx)
+{
+	struct array *true_proc, *false_proc;
+	bool predicate;
+
+	check(3);
+	false_proc = ostack_get_procedure(ctx, 0);
+	true_proc = ostack_get_procedure(ctx, 1);
+	predicate = ostack_get_boolean(ctx, 2);
+	pop(3);
+
+	if (predicate)
+		array_execute(true_proc);
+	else
+		array_execute(false_proc);
+
+	if (--true_proc->base.ref == 0)
+		array_free(true_proc);
+	if (--false_proc->base.ref == 0)
+		array_free(false_proc);
+}
+
+static void _index(struct script *ctx)
+{
+	long n;
+
+	check(1);
+	n = ostack_get_integer(ctx, 0);
+	pop(1);
+
+	check(n+1);
+	return ostack_push(ctx, ostack_copy(ctx, n));
+}
+
+static void _integer (struct script *ctx)
+{
+	obj_t *obj;
+
+	check (1);
+
+	obj = ostack_inplace(ctx, 0);
+	switch (obj_type (*obj)) {
+	case OBJ_TYPE_INTEGER:
+		break;
+	case OBJ_TYPE_REAL:
+		obj->integer = obj->real;
+		obj->type = OBJ_TYPE_INTEGER;
+		break;
+	default:
+		ERR(1);
+	}
+}
+
+static void _interrupt(struct script *ctx)
+{
+	struct array *proc;
+
+	check(1);
+	proc = ostack_get_procedure(ctx, 0);
+	pop(1);
+
+	igt_while_interruptible(true)
+		array_execute(proc);
+
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _le(struct script *ctx)
+{
+	int cmp;
+
+	check (2);
+	cmp = obj_compare(ostack_get(ctx, 1), ostack_get (ctx, 0));
+	pop (2);
+
+	ostack_push_boolean(ctx, cmp <= 0);
+}
+
+static void _lt(struct script *ctx)
+{
+	int cmp;
+
+	check (2);
+	cmp = obj_compare(ostack_get(ctx, 1), ostack_get (ctx, 0));
+	pop (2);
+
+	ostack_push_boolean(ctx, cmp < 0);
+}
+
+static void _mark(struct script *ctx)
+{
+	return ostack_push_mark (ctx);
+}
+
+static void _ne(struct script *ctx)
+{
+	int cmp;
+
+	check (2);
+	cmp = obj_compare(ostack_get (ctx, 1), ostack_get (ctx, 0));
+	pop (2);
+
+	ostack_push_boolean (ctx, cmp);
+}
+
+static void _neg(struct script *ctx)
+{
+	obj_t *obj;
+
+	check (1);
+
+	obj = ostack_inplace(ctx, 0);
+	switch (obj_type(*obj)) {
+	case OBJ_TYPE_INTEGER:
+		obj->integer = -obj->integer;
+		break;
+	case OBJ_TYPE_REAL:
+		obj->real = -obj->real;
+		break;
+	default:
+		ERR(1);
+	}
+}
+
+static void _not(struct script *ctx)
+{
+	obj_t *obj;
+
+	check (1);
+
+	obj = ostack_inplace(ctx, 0);
+	switch (obj_type(*obj)) {
+	case OBJ_TYPE_BOOLEAN:
+		obj->boolean = !obj->boolean;
+		break;
+	case OBJ_TYPE_INTEGER:
+		obj->type = OBJ_TYPE_BOOLEAN;
+		obj->boolean = !obj->integer;
+		break;
+	case OBJ_TYPE_REAL:
+		obj->type = OBJ_TYPE_BOOLEAN;
+		obj->boolean = obj->real == 0.0;
+		break;
+	default:
+		ERR(1);
+	}
+}
+
+static void _null(struct script *ctx)
+{
+	ostack_push_null(ctx);
+}
+
+static void _mod(struct script *ctx)
+{
+	long x, y;
+
+	check (2);
+	y = ostack_get_integer (ctx, 0);
+	x = ostack_get_integer (ctx, 1);
+	pop (2);
+
+	return ostack_push_integer(ctx, x % y);
+}
+
+static void _mul(struct script *ctx)
+{
+	obj_t A;
+	obj_t B;
+	enum object_type type_a, type_b;
+
+	check (2);
+
+	B = ostack_get (ctx, 0);
+	A = ostack_get (ctx, 1);
+
+	type_a = obj_type (A);
+	ERR (!(type_a == OBJ_TYPE_INTEGER || type_a == OBJ_TYPE_REAL));
+
+	type_b = obj_type (B);
+	ERR (!(type_b == OBJ_TYPE_INTEGER || type_b == OBJ_TYPE_REAL));
+
+	pop (2);
+
+	if (type_a == OBJ_TYPE_REAL && type_b == OBJ_TYPE_REAL)
+		ostack_push_real (ctx, A.real * B.real);
+	else if (type_a == OBJ_TYPE_INTEGER && type_b == OBJ_TYPE_INTEGER)
+		ostack_push_integer (ctx, A.integer * B.integer);
+	else {
+		double v;
+
+		if (type_a == OBJ_TYPE_REAL)
+			v = A.real;
+		else
+			v = A.integer;
+
+		if (type_b == OBJ_TYPE_REAL)
+			v *= B.real;
+		else
+			v *= B.integer;
+
+		return ostack_push_real (ctx, v);
+	}
+}
+
+static void _or(struct script *ctx)
+{
+	obj_t a, b;
+
+	check (2);
+
+	a = ostack_get (ctx, 0);
+	b = ostack_get (ctx, 1);
+	ERR(obj_type (a) != obj_type (b));
+
+	pop (2);
+	switch (obj_type (a)) {
+	case OBJ_TYPE_INTEGER:
+		return ostack_push_integer (ctx, a.integer | b.integer);
+	case OBJ_TYPE_BOOLEAN:
+		return ostack_push_boolean (ctx, a.boolean | b.boolean);
+	default:
+		ERR(1);
+	}
+}
+
+static void _pop(struct script *ctx)
+{
+	check(1);
+	pop(1);
+}
+
+static void _repeat(struct script *ctx)
+{
+	struct array *proc;
+	long count;
+
+	check(2);
+	proc = ostack_get_procedure(ctx, 0);
+	count = ostack_get_integer(ctx, 1);
+	ERR(count < 0);
+	pop(2);
+
+	while (count--)
+		array_execute(proc);
+
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _roll(struct script *ctx)
+{
+	long j, n;
+
+	check(2);
+	j = ostack_get_integer(ctx, 0);
+	n = ostack_get_integer(ctx, 1);
+	pop(2);
+
+	if (n <= 0)
+		return;
+	check(n);
+
+	if (j <= -n || j >= n)
+		j %= n;
+	if (j == 0)
+		return;
+
+	stack_roll(&ctx->ostack, j, n);
+}
+
+static void _set(struct script *ctx)
+{
+	obj_t key, value, dst;
+
+	check (3);
+
+	value = ostack_get(ctx, 0);
+	key = ostack_get(ctx, 1);
+	dst = ostack_get(ctx, 2);
+
+	switch (obj_type (dst)) {
+	case OBJ_TYPE_DICTIONARY:
+		ERR(obj_type(key) != OBJ_TYPE_NAME);
+		dictionary_set(dst.dictionary, key.name, value);
+		break;
+	case OBJ_TYPE_ARRAY:
+		ERR(obj_type(key) != OBJ_TYPE_INTEGER);
+		array_set (dst.array, key.integer, value);
+		break;
+
+	case OBJ_TYPE_STRING:
+#if 0
+		string_put(dst, key, value);
+		break;
+#endif
+	default:
+		ERR(1);
+	}
+
+	pop (2);
+}
+
+static void _timeout(struct script *ctx)
+{
+	struct array *proc;
+	long timeout;
+
+	check(2);
+	proc = ostack_get_procedure(ctx, 0);
+	timeout = ostack_get_integer(ctx, 1);
+	pop(2);
+
+	igt_until_timeout(timeout)
+		array_execute(proc);
+
+	if (--proc->base.ref == 0)
+		array_free(proc);
+}
+
+static void _true(struct script *ctx)
+{
+	ostack_push_boolean (ctx, true);
+}
+
+static void _sub(struct script *ctx)
+{
+	obj_t A, B;
+	enum object_type type_a, type_b;
+
+	check (2);
+
+	B = ostack_get (ctx, 0);
+	A = ostack_get (ctx, 1);
+
+	type_a = obj_type (A);
+	ERR (!(type_a == OBJ_TYPE_INTEGER || type_a == OBJ_TYPE_REAL));
+
+	type_b = obj_type (B);
+	ERR (!(type_b == OBJ_TYPE_INTEGER || type_b == OBJ_TYPE_REAL));
+
+	pop (2);
+
+	if (type_a == OBJ_TYPE_REAL && type_b == OBJ_TYPE_REAL)
+		ostack_push_real (ctx, A.real - B.real);
+	else if (type_a == OBJ_TYPE_INTEGER && type_b == OBJ_TYPE_INTEGER)
+		ostack_push_integer (ctx, A.integer - B.integer);
+	else {
+		double v;
+
+		if (type_a == OBJ_TYPE_REAL)
+			v = A.real;
+		else
+			v = A.integer;
+
+		if (type_b == OBJ_TYPE_REAL)
+			v -= B.real;
+		else
+			v -= B.integer;
+
+		ostack_push_real(ctx, v);
+	}
+}
+
+static void _undef(struct script *ctx)
+{
+	name_t name;
+
+	check (1);
+	name = ostack_get_name(ctx, 0);
+	pop (1);
+
+	name_undefine (ctx, name);
+}
+
+static void _unset(struct script *ctx)
+{
+	obj_t dst;
+	name_t name;
+
+	check (2);
+
+	name = ostack_get_name (ctx, 0);
+	dst = ostack_get (ctx, 1);
+
+	switch (obj_type (dst)) {
+	case OBJ_TYPE_DICTIONARY:
+		dictionary_unset (dst.dictionary, name);
+		break;
+	case OBJ_TYPE_STRING:
+	case OBJ_TYPE_ARRAY:
+	default:
+		ERR(1);
+	}
+
+	pop (1);
+}
+
+static void _waitchildren(struct script *ctx)
+{
+	igt_waitchildren();
+}
+
+static void _xor(struct script *ctx)
+{
+	obj_t a, b;
+
+	check (2);
+	a = ostack_get (ctx, 0);
+	b = ostack_get (ctx, 1);
+	ERR (obj_type (a) != obj_type (b));
+	pop (2);
+
+	switch (obj_type (a)) {
+	case OBJ_TYPE_INTEGER:
+		ostack_push_integer (ctx, a.integer ^ b.integer);
+	case OBJ_TYPE_BOOLEAN:
+		ostack_push_boolean (ctx, a.boolean ^ b.boolean);
+	default:
+		ERR(1);
+	}
+}
+
+static void _debug_print(struct script *ctx)
+{
+	obj_t obj;
+
+	check (1);
+	obj = ostack_get (ctx, 0);
+	switch (obj_type(obj)) {
+	case OBJ_TYPE_NULL:
+		fprintf(stderr, "NULL\n");
+		break;
+
+		/* atomics */
+	case OBJ_TYPE_BOOLEAN:
+		fprintf (stderr, "boolean: %s\n",
+			 obj.boolean ? "true" : "false");
+		break;
+	case OBJ_TYPE_INTEGER:
+		fprintf (stderr, "integer: %ld\n", obj.integer);
+		break;
+	case OBJ_TYPE_MARK:
+		fprintf (stderr, "mark\n");
+		break;
+	case OBJ_TYPE_NAME:
+		fprintf (stderr, "name: %s\n", (char *) obj.name);
+		break;
+	case OBJ_TYPE_OPERATOR:
+		fprintf (stderr, "operator: %p\n", obj.ptr);
+		break;
+	case OBJ_TYPE_REAL:
+		fprintf (stderr, "real: %g\n", obj.real);
+		break;
+
+		/* compound */
+	case OBJ_TYPE_ARRAY:
+		fprintf (stderr, "array\n");
+		break;
+	case OBJ_TYPE_DICTIONARY:
+		fprintf (stderr, "dictionary\n");
+		break;
+	case OBJ_TYPE_FILE:
+		fprintf (stderr, "file\n");
+		break;
+	case OBJ_TYPE_STRING:
+		fprintf (stderr, "string: %s\n", obj.string->string);
+		break;
+
+	default:
+		fprintf (stderr, "unknown type=%d, ptr=%p\n", obj.type, obj.ptr);
+		break;
+	}
+	pop (1);
+}
+
+static const struct opdef {
+	const char *name;
+	void (*op)(struct script *);
+} ops[] = {
+	{ "<<", _mark },
+	{ ">>", end_dict_construction },
+	{ "[", _mark },
+	{ "]", end_array_construction },
+	{ "add", _add },
+	{ "and", _and },
+	{ "array", _array },
+	{ "bind", _bind },
+	{ "bitshift", _bitshift },
+	{ "copy", _copy },
+	{ "cvi", _cvi },
+	{ "cvr", _cvr },
+	{ "def", _def },
+	{ "dict", _dict },
+	{ "div", _div },
+	{ "dup", _duplicate },
+	{ "eq", _eq },
+	{ "exch", _exch },
+	{ "false", _false },
+	{ "for", _for },
+	{ "foreach", _foreach },
+	{ "fork", _fork },
+	{ "ge", _ge },
+	{ "get", _get },
+	{ "gt", _gt },
+	{ "if", _if },
+	{ "ifelse", _ifelse },
+	{ "index", _index },
+	{ "integer", _integer },
+	{ "interrupt", _interrupt },
+	{ "le", _le },
+	{ "lt", _lt },
+	{ "mark", _mark },
+	{ "mod", _mod },
+	{ "mul", _mul },
+	{ "ne", _ne },
+	{ "neg", _neg },
+	{ "not", _not },
+	{ "null", _null },
+	{ "or", _or },
+	{ "pop", _pop },
+	//{ "rand", NULL },
+	{ "repeat", _repeat },
+	{ "roll", _roll },
+	{ "set", _set },
+	{ "sub", _sub },
+	{ "timeout", _timeout },
+	{ "true", _true },
+	//{ "type", NULL },
+	{ "undef", _undef },
+	{ "unset", _unset },
+	{ "waitchildren", _waitchildren },
+	//{ "where", NULL },
+	{ "xor", _xor },
+
+	{ "=", _debug_print },
+
+	{ NULL, NULL },
+};
+
+void script_init_operators(struct script *ctx)
+{
+	struct dictionary *dict = systemdict(ctx);
+
+	for (const struct opdef *def = ops; def->name != NULL; def++)
+		dictionary_set(dict,
+			       name_new_static(ctx, def->name).name,
+			       new_operator(def->op));
+}
+
+static const struct idef {
+	const char *name;
+	long value;
+} idefs[] = {
+	{ NULL, 0 }
+};
+
+static const struct rdef {
+	const char *name;
+	float value;
+} rdefs[] = {
+	{ "math.pi",		M_PI },
+	{ "math.2pi",		2 * M_PI },
+	{ "math.sqrt2",		M_SQRT2 },
+	{ "math.ln2",		M_LN2 },
+
+	{ NULL, 0 }
+};
+
+void script_init_constants(struct script *ctx)
+{
+	struct dictionary *dict = systemdict(ctx);
+
+	for (const struct idef *def = idefs; def->name != NULL; def++)
+		dictionary_set(dict,
+			       name_new_static(ctx, def->name).name,
+			       new_int(def->value));
+
+	for (const struct rdef *def = rdefs; def->name != NULL; def++)
+		dictionary_set(dict,
+			       name_new_static(ctx, def->name).name,
+			       new_real(def->value));
+}
diff --git a/fuzz/scanner.c b/fuzz/scanner.c
new file mode 100644
index 0000000..f473e77
--- /dev/null
+++ b/fuzz/scanner.c
@@ -0,0 +1,938 @@
+#include <limits.h> /* INT_MAX */
+#include <math.h> /* pow */
+#include <stdio.h> /* EOF */
+#include <stdint.h> /* for {INT,UINT}*_{MIN,MAX} */
+#include <stdlib.h> /* malloc/free */
+#include <string.h> /* memset */
+#include <assert.h>
+#include <zlib.h>
+
+#include "script.h"
+
+#if WORDS_BIGENDIAN
+#define le16(x) bswap_16(x)
+#define le32(x) bswap_32(x)
+#define be16(x) x
+#define be32(x) x
+#define to_be32(x) x
+#else
+#define le16(x) x
+#define le32(x) x
+#define be16(x) bswap_16(x)
+#define be32(x) bswap_32(x)
+#define to_be32(x) bswap_32(x)
+#endif
+
+/*
+ * whitespace:
+ * 0 - nul
+ * 9 - tab
+ * A - LF
+ * C - FF
+ * D - CR
+ *
+ * syntax delimiters
+ * ( = 28, ) = 29 - literal strings
+ * < = 3C, > = 3E - hex/base85 strings, dictionary name
+ * [ = 5B, ] = 5D - array
+ * { = 7B, } = 7C - procedure
+ * / = 5C - literal marker
+ * % = 25 - comment
+ */
+
+static void buffer_init(struct script *ctx, struct buffer *buffer)
+{
+    buffer->size = 16384;
+    buffer->base = script_alloc (ctx, 1, buffer->size);
+
+    buffer->ptr = buffer->base;
+    buffer->end = buffer->base + buffer->size;
+}
+
+static void buffer_fini(struct script *ctx, struct buffer *buffer)
+{
+    script_free (ctx, buffer->base);
+}
+
+static void _buffer_grow(struct scanner *scan)
+{
+    int offset = scan->buffer.ptr - scan->buffer.base;
+    char *base = script_realloc(scan->ctx, scan->buffer.base, 2, scan->buffer.size);
+
+    scan->buffer.base = base;
+    scan->buffer.size *= 2;
+    scan->buffer.ptr  = base + offset;
+    scan->buffer.end  = base + scan->buffer.size;
+}
+
+static inline void buffer_check(struct scanner *scan, int count)
+{
+	if (scan->buffer.ptr + count > scan->buffer.end)
+		_buffer_grow(scan);
+}
+
+static inline void buffer_add(struct buffer *buffer, int c)
+{
+	*buffer->ptr++ = c;
+}
+
+static inline void buffer_reset(struct buffer *buffer)
+{
+	buffer->ptr = buffer->base;
+}
+
+static void token_start(struct scanner *scan)
+{
+	buffer_reset (&scan->buffer);
+}
+
+static void token_add(struct scanner *scan, int c)
+{
+	buffer_check(scan, 1);
+	buffer_add(&scan->buffer, c);
+}
+
+static void token_add_unchecked(struct scanner *scan, int c)
+{
+	buffer_add(&scan->buffer, c);
+}
+
+bool parse_number(obj_t *obj, const char *s, int len)
+{
+	int radix = 0;
+	long long mantissa = 0;
+	int exponent = 0;
+	int sign = 1;
+	int decimal = -1;
+	int exponent_sign = 0;
+	const char * const end = s + len;
+
+	switch (*s) {
+	case '0':
+	case '1':
+	case '2':
+	case '3':
+	case '4':
+	case '5':
+	case '6':
+	case '7':
+	case '8':
+	case '9':
+		mantissa = *s - '0';
+	case '+':
+		break;
+	case '-':
+		sign = -1;
+		break;
+	case '.':
+		decimal = 0;
+		break;
+	default:
+		return false;
+	}
+
+	while (++s < end) {
+		if (*s < '0') {
+			if (*s == '.') {
+				if (radix)
+					return false;
+				if (decimal != -1)
+					return false;
+				if (exponent_sign)
+					return false;
+
+				decimal = 0;
+			} else if (*s == '!') {
+				if (radix)
+					return false;
+				if (decimal != -1)
+					return false;
+				if (exponent_sign)
+					return false;
+
+				radix = mantissa;
+				mantissa = 0;
+
+				if (radix < 2 || radix > 36)
+					return false;
+			} else
+				return false;
+		} else if (*s <= '9') {
+			int v = *s - '0';
+			if (radix && v >= radix)
+				return false;
+
+			if (exponent_sign) {
+				exponent = 10 * exponent + v;
+			} else {
+				if (radix)
+					mantissa = radix * mantissa + v;
+				else
+					mantissa = 10 * mantissa + v;
+				if (decimal != -1)
+					decimal++;
+			}
+		} else if (*s == 'E' || * s== 'e') {
+			if (radix == 0) {
+				if (s + 1 == end)
+					return false;
+
+				exponent_sign = 1;
+				if (s[1] == '-') {
+					exponent_sign = -1;
+					s++;
+				} else if (s[1] == '+')
+					s++;
+			} else {
+				int v = 0xe;
+
+				if (v >= radix)
+					return false;
+
+				mantissa = radix * mantissa + v;
+			}
+		} else if (*s < 'A') {
+			return false;
+		} else if (*s <= 'Z') {
+			int v = *s - 'A' + 0xA;
+
+			if (v >= radix)
+				return false;
+
+			mantissa = radix * mantissa + v;
+		} else if (*s < 'a') {
+			return false;
+		} else if (*s <= 'z') {
+			int v = *s - 'a' + 0xa;
+
+			if (v >= radix)
+				return false;
+
+			mantissa = radix * mantissa + v;
+		} else
+			return false;
+	}
+
+	if (exponent_sign || decimal != -1) {
+		if (mantissa == 0) {
+			obj->type = OBJ_TYPE_REAL;
+			obj->real = 0.;
+			return true;
+		} else {
+			int e;
+			double v;
+
+			v = mantissa;
+			e = exponent * exponent_sign;
+			if (decimal != -1)
+				e -= decimal;
+			switch (e) {
+			case -7: v *= 0.0000001; break;
+			case -6: v *= 0.000001; break;
+			case -5: v *= 0.00001; break;
+			case -4: v *= 0.0001; break;
+			case -3: v *= 0.001; break;
+			case -2: v *= 0.01; break;
+			case -1: v *= 0.1; break;
+			case  0: break;
+			case  1: v *= 10; break;
+			case  2: v *= 100; break;
+			case  3: v *= 1000; break;
+			case  4: v *= 10000; break;
+			case  5: v *= 100000; break;
+			case  6: v *= 1000000; break;
+			default:
+				 v *= pow (10, e); /* XXX */
+				 break;
+			}
+
+			obj->type = OBJ_TYPE_REAL;
+			obj->real = sign * v;
+			return true;
+		}
+	} else {
+		obj->type = OBJ_TYPE_INTEGER;
+		obj->integer = sign * mantissa;
+		return true;
+	}
+}
+
+static void token_end(struct scanner *scan, struct file *src)
+{
+	char *s;
+	obj_t obj;
+	int len;
+
+	/*
+	 * Any token that consists entirely of regular characters and
+	 * cannot be interpreted as a number is treated as a name object
+	 * (more precisely, an executable name). All characters except
+	 * delimiters and white-space characters can appear in names,
+	 * including characters ordinarily considered to be punctuation.
+	 */
+
+	if (scan->buffer.ptr == scan->buffer.base)
+		return;
+
+	s = scan->buffer.base;
+	len = scan->buffer.ptr - scan->buffer.base;
+
+	if (!scan->bind) {
+		if (s[0] == '{') { /* special case procedures */
+			if (scan->build_procedure.type != OBJ_TYPE_NULL)
+				stack_push(&scan->procedure_stack,
+					   scan->build_procedure);
+
+			scan->build_procedure = array_new (scan->ctx, 0);
+			scan->build_procedure.type |= OBJ_ATTR_EXECUTABLE;
+			return;
+		} else if (s[0] == '}') {
+			if (scan->build_procedure.type == OBJ_TYPE_NULL)
+				longjmp (scan->ctx->error, -EINVAL);
+
+			if (scan->procedure_stack.len) {
+				obj_t next;
+
+				next = stack_peek(&scan->procedure_stack, 0);
+				array_append(next.array, scan->build_procedure);
+				scan->build_procedure = next;
+				scan->procedure_stack.len--;
+			} else {
+				ostack_push(scan->ctx, scan->build_procedure);
+				scan->build_procedure.type = OBJ_TYPE_NULL;
+			}
+
+			return;
+		}
+	}
+
+	if (s[0] == '/') {
+		if (len >= 2 && s[1] == '/')  /* substituted name */
+			obj = name_lookup (scan->ctx, name_new(scan->ctx, s + 2, len - 2).name);
+		else /* literal name */
+			obj = name_new (scan->ctx, s + 1, len - 1);
+	} else {
+		if (!parse_number (&obj, s, len)) {
+			obj = name_new (scan->ctx, s, len);
+			obj.type |= OBJ_ATTR_EXECUTABLE;
+		}
+	}
+
+	/* consume whitespace after token, before calling the interpreter */
+	if (scan->build_procedure.type != OBJ_TYPE_NULL) {
+		array_append(scan->build_procedure.array, obj);
+	} else if (obj.type & OBJ_ATTR_EXECUTABLE) {
+		obj_execute(scan->ctx, obj);
+		obj_free(obj);
+	} else {
+		ostack_push(scan->ctx, obj);
+	}
+}
+
+static void string_add(struct scanner *scan, int c)
+{
+	buffer_check(scan, 1);
+	buffer_add(&scan->buffer, c);
+}
+
+static void string_end(struct scanner *scan)
+{
+	obj_t obj = string_new(scan->ctx,
+			       scan->buffer.base,
+			       scan->buffer.ptr - scan->buffer.base);
+	if (scan->build_procedure.type != OBJ_TYPE_NULL)
+		array_append(scan->build_procedure.array, obj);
+	else
+		ostack_push(scan->ctx, obj);
+}
+
+static int hex_value(int c)
+{
+	if (c < '0')
+		return EOF;
+	if (c <= '9')
+		return c - '0';
+	c |= 32;
+	if (c < 'a')
+		return EOF;
+	if (c <= 'f')
+		return c - 'a' + 0xa;
+	return EOF;
+}
+
+static void hex_add(struct scanner *scan, int c)
+{
+	if (scan->accumulator_count == 0) {
+		scan->accumulator |= hex_value (c) << 4;
+		scan->accumulator_count = 1;
+	} else {
+		scan->accumulator |= hex_value (c) << 0;
+		buffer_check(scan, 1);
+		buffer_add(&scan->buffer, scan->accumulator);
+
+		scan->accumulator = 0;
+		scan->accumulator_count = 0;
+	}
+}
+
+static void hex_end(struct scanner *scan)
+{
+	obj_t obj;
+
+	if (scan->accumulator_count)
+		hex_add(scan, '0');
+
+	obj = string_new(scan->ctx,
+			 scan->buffer.base,
+			 scan->buffer.ptr - scan->buffer.base);
+
+	if (scan->build_procedure.type != OBJ_TYPE_NULL)
+		array_append(scan->build_procedure.array, obj);
+	else
+		ostack_push(scan->ctx, obj);
+}
+
+static void base85_add(struct scanner *scan, int c)
+{
+    if (c == 'z') {
+	if (scan->accumulator_count != 0)
+	    longjmp (scan->ctx->error, -EINVAL);
+
+	buffer_check (scan, 4);
+	buffer_add (&scan->buffer, 0);
+	buffer_add (&scan->buffer, 0);
+	buffer_add (&scan->buffer, 0);
+	buffer_add (&scan->buffer, 0);
+    } else if (c < '!' || c > 'u') {
+	longjmp (scan->ctx->error, -EINVAL);
+    } else {
+	scan->accumulator = scan->accumulator*85 + c - '!';
+	if (++scan->accumulator_count == 5) {
+	    buffer_check (scan, 4);
+	    buffer_add (&scan->buffer, (scan->accumulator >> 24) & 0xff);
+	    buffer_add (&scan->buffer, (scan->accumulator >> 16) & 0xff);
+	    buffer_add (&scan->buffer, (scan->accumulator >>  8) & 0xff);
+	    buffer_add (&scan->buffer, (scan->accumulator >>  0) & 0xff);
+
+	    scan->accumulator = 0;
+	    scan->accumulator_count = 0;
+	}
+    }
+}
+
+static void base85_end(struct scanner *scan, bool deflate)
+{
+	obj_t obj;
+
+	buffer_check(scan, 4);
+
+	switch (scan->accumulator_count) {
+	case 0:
+		break;
+	case 1:
+		longjmp (scan->ctx->error, -EINVAL);
+		break;
+
+	case 2:
+		scan->accumulator = scan->accumulator * (85*85*85) + 85*85*85 -1;
+		buffer_add (&scan->buffer, (scan->accumulator >> 24) & 0xff);
+		break;
+	case 3:
+		scan->accumulator = scan->accumulator * (85*85) + 85*85 -1;
+		buffer_add (&scan->buffer, (scan->accumulator >> 24) & 0xff);
+		buffer_add (&scan->buffer, (scan->accumulator >> 16) & 0xff);
+		break;
+	case 4:
+		scan->accumulator = scan->accumulator * 85 + 84;
+		buffer_add (&scan->buffer, (scan->accumulator >> 24) & 0xff);
+		buffer_add (&scan->buffer, (scan->accumulator >> 16) & 0xff);
+		buffer_add (&scan->buffer, (scan->accumulator >>  8) & 0xff);
+		break;
+	}
+
+	if (deflate) {
+		uLongf len = be32(*(uint32_t *) scan->buffer.base);
+		Bytef *source = (Bytef *)(scan->buffer.base + sizeof (uint32_t));
+
+		obj = string_deflate_new(scan->ctx, source,
+					 (Bytef *) scan->buffer.ptr - source, len);
+	} else {
+		obj = string_new(scan->ctx,
+				 scan->buffer.base,
+				 scan->buffer.ptr - scan->buffer.base);
+	}
+
+	if (scan->build_procedure.type != OBJ_TYPE_NULL)
+		array_append(scan->build_procedure.array, obj);
+	else
+		ostack_push(scan->ctx, obj);
+}
+
+static void base64_add(struct scanner *scan, int c)
+{
+	int val;
+
+	/* Convert Base64 character to its 6 bit nibble */
+	val = scan->accumulator;
+	if (c =='/') {
+		val = (val << 6) | 63;
+	} else if (c =='+') {
+		val = (val << 6) | 62;
+	} else if (c >='A' && c <='Z') {
+		val = (val << 6) | (c -'A');
+	} else if (c >='a' && c <='z') {
+		val = (val << 6) | (c -'a' + 26);
+	} else if (c >='0' && c <='9') {
+		val = (val << 6) | (c -'0' + 52);
+	}
+
+	buffer_check (scan, 1);
+	switch (scan->accumulator_count++) {
+	case 0:
+		break;
+
+	case 1:
+		buffer_add (&scan->buffer, (val >> 4) & 0xFF);
+		val &= 0xF;
+		break;
+
+	case 2:
+		buffer_add (&scan->buffer, (val >> 2) & 0xFF);
+		val &= 0x3;
+		break;
+
+	case 3:
+		buffer_add (&scan->buffer, (val >> 0) & 0xFF);
+		scan->accumulator_count = 0;
+		val = 0;
+		break;
+	}
+
+	if (c == '=') {
+		scan->accumulator_count = 0;
+		scan->accumulator = 0;
+	} else {
+		scan->accumulator = val;
+	}
+}
+
+static void base64_end(struct scanner *scan)
+{
+	obj_t obj;
+
+	switch (scan->accumulator_count) {
+	case 0:
+		break;
+	case 2:
+		base64_add(scan, (scan->accumulator << 2) & 0x3f);
+		base64_add(scan, '=');
+		break;
+	case 1:
+		base64_add(scan, (scan->accumulator << 4) & 0x3f);
+		base64_add(scan, '=');
+		base64_add(scan, '=');
+		break;
+	}
+
+	obj = string_new(scan->ctx,
+			 scan->buffer.base,
+			 scan->buffer.ptr - scan->buffer.base);
+
+	if (scan->build_procedure.type != OBJ_TYPE_NULL)
+		array_append(scan->build_procedure.array, obj);
+	else
+		ostack_push(scan->ctx, obj);
+}
+
+void scan_file(struct file *src)
+{
+	struct script *ctx = src->base.ctx;
+	struct scanner *scan = &ctx->scanner;
+	int c, next;
+	int deflate = 0;
+	int string_p;
+
+scan_none:
+	while ((c = file_getc (src)) != EOF) {
+		obj_t obj = { OBJ_TYPE_NULL };
+
+		switch (c) {
+		case 0xa:
+			scan->line_number++;
+		case 0x0:
+		case 0x9:
+		case 0xc:
+		case 0xd:
+		case 0x20: /* ignore whitespace */
+			break;
+
+		case '%':
+			goto scan_comment;
+
+		case '(':
+			goto scan_string;
+
+		case '[': /* needs special case */
+		case ']':
+		case '{':
+		case '}':
+			token_start (scan);
+			token_add_unchecked (scan, c);
+			token_end (scan, src);
+			goto scan_none;
+
+		case '<':
+			next = file_getc (src);
+			switch (next) {
+			case EOF:
+				file_putc (src, '<');
+				return;
+			case '<':
+				/* dictionary name */
+				token_start (scan);
+				token_add_unchecked (scan, '<');
+				token_add_unchecked (scan, '<');
+				token_end (scan, src);
+				goto scan_none;
+			case '|':
+				deflate = 1;
+			case '~':
+				goto scan_base85;
+			case '{':
+				goto scan_base64;
+			default:
+				file_putc (src, next);
+				goto scan_hex;
+			}
+			break;
+
+		case '#': /* PDF 1.2 escape code */
+			{
+				int c_hi = file_getc (src);
+				int c_lo = file_getc (src);
+				c = (hex_value (c_hi) << 4) | hex_value (c_lo);
+			}
+			/* fall-through */
+		default:
+			/* XXX can use 128+ for binary tokens */
+			token_start(scan);
+			token_add_unchecked (scan, c);
+			goto scan_token;
+		}
+
+		if (obj.type != OBJ_TYPE_NULL) {
+			if (scan->build_procedure.type != OBJ_TYPE_NULL) {
+				array_append(scan->build_procedure.array, obj);
+			} else if (obj.type & OBJ_ATTR_EXECUTABLE) {
+				obj_execute(ctx, obj);
+				obj_free(obj);
+			} else {
+				ostack_push(ctx, obj);
+			}
+		}
+	}
+	return;
+
+scan_token:
+	while ((c = file_getc (src)) != EOF) {
+		switch (c) {
+		case 0xa:
+			scan->line_number++;
+		case 0x0:
+		case 0x9:
+		case 0xc:
+		case 0xd:
+		case 0x20:
+			token_end (scan, src);
+			goto scan_none;
+
+			/* syntax delimiters */
+		case '%':
+			token_end (scan, src);
+			goto scan_comment;
+			/* syntax error? */
+		case '(':
+			token_end (scan, src);
+			goto scan_string;
+			/* XXX syntax error? */
+		case ')':
+			token_end (scan, src);
+			goto scan_none;
+		case '/':
+			/* need to special case '^//?' */
+			if (scan->buffer.ptr > scan->buffer.base+1 ||
+			    scan->buffer.base[0] != '/')
+			{
+				token_end (scan, src);
+				token_start (scan);
+			}
+			token_add_unchecked (scan, '/');
+			goto scan_token;
+
+		case '{':
+		case '}':
+		case ']':
+			token_end(scan, src);
+			token_start(scan);
+			token_add_unchecked(scan, c);
+			token_end(scan, src);
+			goto scan_none;
+
+		case '<':
+			file_putc (src, '<');
+			token_end (scan, src);
+			goto scan_none;
+
+		case '#': /* PDF 1.2 escape code */
+			{
+				int c_hi = file_getc (src);
+				int c_lo = file_getc (src);
+				c = (hex_value (c_hi) << 4) | hex_value (c_lo);
+			}
+			/* fall-through */
+		default:
+			token_add (scan, c);
+			break;
+		}
+	}
+	token_end(scan, src);
+	return;
+
+scan_comment:
+	/* discard until newline */
+	while ((c = file_getc (src)) != EOF) {
+		switch (c) {
+		case 0xa:
+			scan->line_number++;
+		case 0xc:
+			goto scan_none;
+		}
+	}
+	return;
+
+scan_string:
+	buffer_reset (&scan->buffer);
+	string_p = 1;
+	while ((c = file_getc (src)) != EOF) {
+		switch (c) {
+		case '\\': /* escape */
+			next = file_getc (src);
+			switch (next) {
+			case EOF:
+				longjmp (ctx->error, -EINVAL);
+
+			case 'n':
+				string_add (scan, '\n');
+				break;
+			case 'r':
+				string_add (scan, '\r');
+				break;
+			case 't':
+				string_add (scan, '\t');
+				break;
+			case 'b':
+				string_add (scan, '\b');
+				break;
+			case 'f':
+				string_add (scan, '\f');
+				break;
+			case '\\':
+				string_add (scan, '\\');
+				break;
+			case '(':
+				string_add (scan, '(');
+				break;
+			case ')':
+				string_add (scan, ')');
+				break;
+
+			case '0': case '1': case '2': case '3':
+			case '4': case '5': case '6': case '7':
+				{ /* octal code: \d{1,3} */
+					int i;
+
+					c = next - '0';
+
+					for (i = 0; i < 2; i++) {
+						next = file_getc (src);
+						switch (next) {
+						case EOF:
+							return;
+
+						case '0': case '1': case '2': case '3':
+						case '4': case '5': case '6': case '7':
+							c = 8*c + next-'0';
+							break;
+
+						default:
+							file_putc (src, next);
+							goto octal_code_done;
+						}
+					}
+octal_code_done:
+					string_add (scan, c);
+				}
+				break;
+
+			case 0xa:
+				/* skip the newline */
+				next = file_getc (src); /* might be compound LFCR */
+				switch (next) {
+				case EOF:
+					return;
+				case 0xc:
+					break;
+				default:
+					file_putc (src, next);
+					break;
+				}
+				scan->line_number++;
+				break;
+			case 0xc:
+				break;
+
+			default:
+				/* ignore the '\' */
+				break;
+			}
+			break;
+
+		case '(':
+			string_p++;
+			string_add (scan, c);
+			break;
+
+		case ')':
+			if (--string_p == 0) {
+				string_end (scan);
+				goto scan_none;
+			}
+			/* fall through */
+		default:
+			string_add (scan, c);
+			break;
+		}
+	}
+	longjmp (ctx->error, -EINVAL);
+
+scan_hex:
+	buffer_reset (&scan->buffer);
+	scan->accumulator_count = 0;
+	scan->accumulator = 0;
+	while ((c = file_getc (src)) != EOF) {
+		switch (c) {
+		case 0xa:
+			scan->line_number++;
+		case 0x0:
+		case 0x9:
+		case 0xc:
+		case 0xd:
+		case 0x20: /* ignore whitespace */
+			break;
+
+		case '>':
+			hex_end (scan); /* fixup odd digit with '0' */
+			goto scan_none;
+
+		case '0':
+		case '1':
+		case '2':
+		case '3':
+		case '4':
+		case '5':
+		case '6':
+		case '7':
+		case '8':
+		case '9':
+		case 'a':
+		case 'b':
+		case 'c':
+		case 'd':
+		case 'e':
+		case 'f':
+		case 'A':
+		case 'B':
+		case 'C':
+		case 'D':
+		case 'E':
+		case 'F':
+			hex_add(scan, c);
+			break;
+
+		default:
+			longjmp(ctx->error, -EINVAL);
+		}
+	}
+	longjmp (ctx->error, -EINVAL);
+
+scan_base85:
+	buffer_reset (&scan->buffer);
+	scan->accumulator = 0;
+	scan->accumulator_count = 0;
+	while ((c = file_getc (src)) != EOF) {
+		switch (c) {
+		case '~':
+			next = file_getc (src);
+			switch (next) {
+			case EOF:
+				return;
+
+			case '>':
+				base85_end(scan, deflate);
+				deflate = 0;
+				goto scan_none;
+			}
+			file_putc (src, next);
+
+			/* fall-through */
+		default:
+			base85_add(scan, c);
+			break;
+		}
+	}
+	longjmp (ctx->error, -EINVAL);
+
+scan_base64:
+	buffer_reset (&scan->buffer);
+	scan->accumulator = 0;
+	scan->accumulator_count = 0;
+	while ((c = file_getc (src)) != EOF) {
+		switch (c) {
+		case '}':
+			next = file_getc (src);
+			switch (next) {
+			case EOF:
+				return;
+
+			case '>':
+				base64_end (scan);
+				goto scan_none;
+			}
+			longjmp (ctx->error, -EINVAL);
+
+		default:
+			base64_add(scan, c);
+			break;
+		}
+	}
+	longjmp (ctx->error, -EINVAL);
+}
+
+void scanner_init(struct script *ctx, struct scanner *scanner)
+{
+    memset(scanner, 0, sizeof(struct scanner));
+    scanner->ctx = ctx;
+
+    buffer_init(ctx, &scanner->buffer);
+    stack_init(ctx, &scanner->procedure_stack, 4);
+
+    scanner->bind = 0;
+}
+
+void scanner_fini (struct scanner *scanner)
+{
+	buffer_fini(scanner->ctx, &scanner->buffer);
+	stack_fini(&scanner->procedure_stack);
+	if (scanner->build_procedure.type != OBJ_TYPE_NULL)
+		obj_free (scanner->build_procedure);
+}
diff --git a/fuzz/script.h b/fuzz/script.h
new file mode 100644
index 0000000..d7c1bb0
--- /dev/null
+++ b/fuzz/script.h
@@ -0,0 +1,532 @@
+#ifndef SCRIPT_H
+#define SCRIPT_H
+
+#include <errno.h>
+#include <setjmp.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <byteswap.h>
+
+struct script;
+
+enum object_type {
+    OBJ_TYPE_NULL = 0,
+
+    /* atomics */
+    OBJ_TYPE_BOOLEAN,
+    OBJ_TYPE_INTEGER,
+    OBJ_TYPE_MARK,
+    OBJ_TYPE_NAME,
+    OBJ_TYPE_OPERATOR,
+    OBJ_TYPE_REAL,
+
+    /* compound */
+    OBJ_TYPE_ARRAY = 0x8,
+    OBJ_TYPE_DICTIONARY,
+    OBJ_TYPE_FILE,
+    OBJ_TYPE_STRING,
+
+    /* extension */
+    OBJ_TYPE_EXTENSION_0 = 0x10,
+} object_type_t;
+
+#define OBJ_IS_ATOM(OBJ) (((OBJ)->type & OBJ_TYPE_MASK) < 0x08)
+#define OBJ_IS_COMPOUND(OBJ) ((OBJ).type & 0xf8)
+
+enum { /* attributes */
+    OBJ_ATTR_EXECUTABLE = 1 << 8,
+    OBJ_ATTR_WRITABLE   = 1 << 9
+};
+#define OBJ_ATTR_MASK (OBJ_ATTR_EXECUTABLE | OBJ_ATTR_WRITABLE)
+#define OBJ_TYPE_MASK (~OBJ_ATTR_MASK)
+
+typedef bool
+(*hash_predicate_func_t) (void *entry);
+
+typedef void
+(*hash_callback_func_t) (void *entry,
+			     void *closure);
+
+typedef bool
+(*hash_keys_equal_func_t) (const void *key_a, const void *key_b);
+
+typedef long name_t;
+
+typedef struct stack_object {
+    enum object_type type;
+    union {
+	bool boolean;
+	long integer;
+	float real;
+
+	void *ptr;
+	name_t name;
+	void (*op)(struct script *);
+
+	struct heap_object *object;
+	struct extension_object *extension;
+
+	struct array *array;
+	struct dictionary *dictionary;
+	struct file *file;
+	struct string *string;
+    };
+} obj_t;
+
+struct heap_object {
+    enum object_type type;
+    unsigned int ref;
+    struct script *ctx;
+};
+
+struct hash_entry {
+    unsigned long hash;
+};
+
+struct hash_table_arrangement {
+    unsigned long high_water_mark;
+    unsigned long size;
+    unsigned long rehash;
+};
+
+struct hash_table {
+	struct script *ctx;
+	hash_keys_equal_func_t keys_equal;
+
+	const struct hash_table_arrangement *arrangement;
+	struct hash_entry **entries;
+
+	unsigned long live_entries;
+	unsigned long used_entries;
+	unsigned long iterating;   /* Iterating, no insert, no resize */
+};
+
+
+/* simple, embedded doubly-linked links */
+struct script_list {
+    struct script_list *next, *prev;
+};
+
+struct buffer {
+    char *base, *ptr, *end;
+    unsigned int size;
+};
+
+struct stack {
+	struct script *ctx;
+	obj_t *objects;
+	long len;
+	long size;
+};
+
+struct array {
+    struct heap_object base;
+    struct stack stack;
+};
+
+struct dictionary_entry {
+    struct hash_entry hash_entry;
+    obj_t obj;
+};
+
+struct dictionary {
+    struct heap_object base;
+    struct hash_table ht;
+};
+
+struct string {
+    struct heap_object base;
+    long len;
+    long deflate;
+    enum {
+	NONE,
+	ZLIB,
+	LZO,
+    } method;
+    char *string;
+};
+
+struct file_filter_funcs {
+    int (*filter_getc) (struct file *);
+    void (*filter_putc) (struct file *, int);
+    int (*filter_read) (struct file *, uint8_t *, int);
+    void (*filter_destroy) (struct script *, void *);
+};
+
+struct file {
+    struct heap_object base;
+    enum {
+	STDIO,
+	BYTES,
+	PROCEDURE,
+	FILTER
+    } type;
+    unsigned int flags;
+    void *src;
+    void *data;
+    uint8_t *bp;
+    int rem;
+    const struct file_filter_funcs *filter;
+};
+
+#if 0
+union union_object {
+    void *ptr[2];
+    struct stack stack;
+    struct array arry;
+    struct dictionary dictionary;
+    csi_matrix_t matrix;
+    struct string string;
+    struct file file;
+    obj_t object;
+};
+#endif
+
+struct scanner {
+    struct script *ctx;
+
+    int depth;
+    int bind;
+
+    struct buffer buffer;
+    struct stack procedure_stack;
+    obj_t build_procedure;
+
+    unsigned int accumulator;
+    unsigned int accumulator_count;
+
+    unsigned int line_number;
+};
+
+struct perm_chunk;
+
+struct script {
+	jmp_buf error;
+	void *private;
+
+	struct hash_table strings;
+
+	struct stack ostack;
+	struct stack dstack;
+
+	struct scanner scanner;
+
+	struct perm_chunk *perm_chunk;
+	struct {
+		struct perm_chunk *chunk;
+		void *free_list;
+	} slabs[16];
+	struct array *free_array;
+	struct dictionary *free_dictionary;
+	struct string *free_string;
+};
+
+struct script_operator_def {
+    const char *name;
+    void (*op)(struct script *);
+};
+
+struct script_integer_constant_def {
+    const char *name;
+    long value;
+};
+
+struct script_real_constant_def {
+    const char *name;
+    float value;
+};
+
+static inline obj_t new_heap_object(struct heap_object *obj,
+			     struct script *ctx,
+			     enum object_type type)
+{
+	obj->type = type;
+	obj->ref = 1;
+	obj->ctx = ctx;
+
+	return (obj_t){.type = type, .object = obj};
+}
+
+struct extension {
+	void (*free)(void *);
+};
+
+struct extension_object {
+	struct heap_object heap;
+	const struct extension *ops;
+};
+
+static inline obj_t new_extension_object(struct extension_object *obj,
+					 struct script *ctx,
+					 enum object_type type,
+					 const struct extension *ops)
+{
+	obj->heap.type = type;
+	obj->heap.ref = 1;
+	obj->heap.ctx = ctx;
+	obj->ops = ops;
+
+	return (obj_t){.type = type, .extension = obj};
+}
+
+obj_t file_new(struct script *ctx, const char *path, const char *mode);
+obj_t file_new_for_stream (struct script *ctx, FILE *stream);
+obj_t file_new_for_bytes (struct script *ctx,
+		const char *bytes, unsigned int length);
+obj_t file_new_from_string (struct script *ctx, struct string *src);
+obj_t file_new_ascii85_decode (struct script *ctx,
+		struct dictionary *dict, obj_t src);
+obj_t file_new_deflate_decode (struct script *ctx,
+		struct dictionary *dict,
+		obj_t src);
+void file_execute (struct file *obj);
+int file_getc (struct file *obj);
+int file_read (struct file *obj, void *buf, int len);
+void file_putc (struct file *obj, int c);
+void file_flush(struct file *obj);
+void file_close(struct file *obj);
+void file_free(struct file *obj);
+obj_t file_as_string(struct file *file);
+
+void
+hash_table_init(struct script *ctx,
+		struct hash_table *ht,
+		hash_keys_equal_func_t keys_equal);
+
+void
+hash_table_fini(struct hash_table *ht);
+
+void *
+hash_table_lookup(struct hash_table *ht, const struct hash_entry *key);
+
+void *
+hash_table_lookup_unique(struct hash_table *ht, unsigned long id);
+
+void
+hash_table_insert(struct hash_table *ht,
+		  struct hash_entry *entry);
+
+void
+hash_table_remove(struct hash_table *ht,
+		  struct hash_entry *key);
+
+void
+hash_table_foreach (struct hash_table	      *ht,
+		    hash_callback_func_t  hash_callback,
+		    void			      *closure);
+
+void *script_alloc(struct script *ctx, long count, long size);
+void * script_alloc0(struct script *ctx, long count, long size);
+void * script_realloc(struct script *ctx, void *ptr, long count, long size);
+void script_free(struct script *ctx, void *ptr);
+
+void * script_slab_alloc(struct script *ctx, int size);
+void script_slab_free(struct script *ctx, void *ptr, int size);
+
+void * script_perm_alloc(struct script *ctx, int size);
+
+void name_define(struct script *ctx, name_t name, obj_t obj);
+obj_t name_lookup(struct script *ctx, name_t name);
+void name_undefine(struct script *ctx, name_t name);
+
+void *intern_string(struct script *ctx, const char *str, int len);
+
+obj_t array_new(struct script *ctx, long initial_size);
+obj_t array_get(struct array *array, long elem);
+void array_set(struct array *array, long elem, obj_t value);
+void array_append(struct array *array, obj_t obj);
+void array_execute(struct array *array);
+void array_free(struct array *array);
+
+static inline obj_t
+new_bool(bool v)
+{
+	return (obj_t){.type = OBJ_TYPE_BOOLEAN, .boolean =v };
+}
+
+obj_t dictionary_new(struct script *ctx);
+void dictionary_set(struct dictionary *dict, name_t name, obj_t value);
+obj_t dictionary_get(struct dictionary *dict, name_t name);
+bool dictionary_has(struct dictionary *dict, name_t name);
+void dictionary_unset(struct dictionary *dict, name_t name);
+void dictionary_free(struct dictionary *dict);
+
+static inline obj_t
+new_int(long v)
+{
+	return (obj_t){.type = OBJ_TYPE_INTEGER, .integer = v};
+}
+
+obj_t name_new(struct script *ctx, const char *str, int len);
+obj_t name_new_static(struct script *ctx, const char *str);
+
+static inline obj_t
+new_operator(void (*op)(struct script *))
+{
+	return (obj_t){.type = OBJ_TYPE_OPERATOR | OBJ_ATTR_EXECUTABLE, .op=op};
+}
+
+static inline obj_t
+new_real(float v)
+{
+	return (obj_t){ .type = OBJ_TYPE_REAL, .real = v};
+}
+
+obj_t string_new(struct script *ctx, const char *str, int len);
+obj_t string_deflate_new (struct script *ctx, void *bytes, int in_len, int out_len);
+obj_t string_new_from_bytes (struct script *ctx, char *bytes, unsigned int len);
+void string_free(struct string *string);
+
+void obj_execute(struct script *ctx, obj_t obj);
+static inline obj_t obj_reference(obj_t obj)
+{
+	if (OBJ_IS_COMPOUND(obj))
+		obj.object->ref++;
+
+	return obj;
+}
+void obj_free(obj_t obj);
+obj_t obj_as_file(struct script *ctx, obj_t src);
+int obj_compare(obj_t a, obj_t b);
+
+void scan_file(struct file *src);
+
+void scanner_init(struct script *ctx, struct scanner *scanner);
+void scanner_fini(struct scanner *scanner);
+bool parse_number(obj_t *obj, const char *s, int len);
+
+void stack_init(struct script *ctx, struct stack *stack, long size);
+void stack_roll(struct stack *stack, long mod, long n);
+void stack_grow(struct stack *stack, long cnt);
+void stack_push_internal(struct stack *stack, obj_t obj);
+obj_t stack_peek(struct stack *stack, long i);
+void stack_pop(struct stack *stack, long count);
+void stack_exch(struct stack *stack);
+void stack_fini(struct stack *stack);
+
+static inline int obj_type(const obj_t obj)
+{
+    return obj.type & OBJ_TYPE_MASK;
+}
+
+static inline bool obj_is_procedure(const obj_t obj)
+{
+    return obj.type == (OBJ_TYPE_ARRAY | OBJ_ATTR_EXECUTABLE);
+}
+
+static inline bool obj_is_number(const obj_t obj)
+{
+    switch (obj_type(obj)) {
+    case OBJ_TYPE_BOOLEAN:
+    case OBJ_TYPE_INTEGER:
+    case OBJ_TYPE_REAL:
+	return true;
+
+    default:
+	return false;
+    }
+}
+
+static inline double
+number_get_value (const obj_t obj)
+{
+    switch (obj_type(obj)) {
+    case OBJ_TYPE_BOOLEAN: return obj.boolean;
+    case OBJ_TYPE_INTEGER: return obj.integer;
+    case OBJ_TYPE_REAL: return obj.real;
+    default: return 0.;
+    }
+}
+
+static inline void
+stack_push(struct stack *stack, obj_t obj)
+{
+	if (stack->len == stack->size)
+		return stack_push_internal(stack, obj);
+
+	stack->objects[stack->len++] = obj;
+}
+
+static inline void
+ostack_check(struct script *ctx, long count)
+{
+    if (ctx->ostack.len < count)
+	    longjmp(ctx->error, -EINVAL);
+}
+
+static inline obj_t ostack_get(struct script *ctx, long i)
+{
+    return ctx->ostack.objects[ctx->ostack.len - i -1];
+}
+
+static inline obj_t ostack_copy(struct script *ctx, long i)
+{
+    return obj_reference(ostack_get(ctx, i));
+}
+
+static inline obj_t *ostack_inplace(struct script *ctx, long i)
+{
+    return &ctx->ostack.objects[ctx->ostack.len - i -1];
+}
+
+static inline void ostack_pop(struct script *ctx, long count)
+{
+    do
+	obj_free(ctx->ostack.objects[--ctx->ostack.len]);
+    while (--count);
+}
+
+static inline void ostack_push_copy(struct script *ctx, obj_t obj)
+{
+    stack_push(&ctx->ostack, obj_reference(obj));
+}
+
+static inline void ostack_push(struct script *ctx, obj_t obj)
+{
+    return stack_push(&ctx->ostack, obj);
+}
+
+static inline void ostack_push_boolean (struct script *ctx, bool v)
+{
+    return stack_push(&ctx->ostack, new_bool(v));
+}
+static inline void ostack_push_integer(struct script *ctx, long v)
+{
+    return stack_push(&ctx->ostack, new_int(v));
+}
+static inline void ostack_push_mark (struct script *ctx)
+{
+    return stack_push(&ctx->ostack, (obj_t){.type = OBJ_TYPE_MARK});
+}
+static inline obj_t null(void)
+{
+    return (obj_t){.type = OBJ_TYPE_NULL};
+}
+static inline bool is_null(obj_t obj)
+{
+	return obj.type == OBJ_TYPE_NULL;
+}
+static inline void ostack_push_null(struct script *ctx)
+{
+    return stack_push(&ctx->ostack, null());
+}
+static inline void ostack_push_real(struct script *ctx, float v)
+{
+    return stack_push(&ctx->ostack, new_real(v));
+}
+
+struct dictionary *systemdict(struct script *ctx);
+void systemdict_execute(struct script *ctx, const char *name);
+struct dictionary *extensiondict(struct script *ctx);
+void script_init_operators(struct script *ctx);
+void script_init_constants(struct script *ctx);
+
+int script_init(struct script *ctx);
+int script_run(struct script *ctx, const char *filename);
+void script_fini(struct script *ctx);
+
+void script_print_strings(struct script *ctx, FILE *file);
+
+#endif /* SCRIPT_H */
diff --git a/fuzz/scripts/context.gem b/fuzz/scripts/context.gem
new file mode 100644
index 0000000..822744e
--- /dev/null
+++ b/fuzz/scripts/context.gem
@@ -0,0 +1,22 @@
+(intel) driver
+
+/engines get { % driver engine --
+  /engine exch def
+  << /size 4096 >> object { % driver object child --
+    3 -1 roll { % object child driver batch
+      4 -2 roll dup store
+    } batch << /engine engine /context 5 index context >> exec
+  } -1 exch fork waitchildren
+  verify
+} foreach
+
+<< /size 4096 >> object exch
+{ % object driver child --
+  exch /engines get { /engine exch def % object child driver --
+    (intel) driver { % object child driver driver batch --
+      4 index 4 index engine store
+    } batch << /engine engine >> exec
+    pop pop
+  } foreach
+} -1 exch fork waitchildren
+exch verify
diff --git a/fuzz/scripts/store.gem b/fuzz/scripts/store.gem
new file mode 100644
index 0000000..1fe48e7
--- /dev/null
+++ b/fuzz/scripts/store.gem
@@ -0,0 +1,16 @@
+(intel) driver {
+  << /size 4096 >> object /global exch def
+  -1 { 1 add { pop exch % count driver --
+    << /size 4096 /caching 0 >> object exch
+    << /size 4096 /caching 1 >> object exch
+    /engines get { /engine exch def % count uncached cached driver --
+      { % count uncached cached driver batch --
+	global 5 index engine store
+	3 index engine 16!c0ffee store
+	2 index engine 16!deadbeef store
+      } batch << /engine engine >> exec pop
+    } foreach exch verify exch verify
+  } 1 exch fork } 1024 exch repeat waitchildren
+  global verify
+  pop
+} bind 10 exch timeout
diff --git a/fuzz/scripts/sync.gem b/fuzz/scripts/sync.gem
new file mode 100644
index 0000000..ef8fa06
--- /dev/null
+++ b/fuzz/scripts/sync.gem
@@ -0,0 +1,17 @@
+(intel) driver /engines get
+{ % driver engine --
+  { % driver engine child --
+    pop exch null batch
+    { { % engine driver batch --
+	<< /engine 4 index >> exec wait
+    } interrupt } 10 exch timeout
+  } 1 exch fork pop
+} bind foreach waitchildren
+
+{ % driver child --
+  pop null batch exch % batch driver --
+  { { /engines get { % batch driver engine --
+    3 -1 roll exch % driver batch engine --
+    << /engine 3 -1 roll >> exec exch % batch driver --
+  } foreach exch wait exch } interrupt } 10 exch timeout
+} bind -1 exch fork waitchildren
diff --git a/fuzz/scripts/write.gem b/fuzz/scripts/write.gem
new file mode 100644
index 0000000..a9228a9
--- /dev/null
+++ b/fuzz/scripts/write.gem
@@ -0,0 +1,14 @@
+(intel) driver {
+  << /size 4096 >> object /global exch def
+  -1 { 1 add { pop exch % count driver --
+    << /size 4096 /caching 0 >> object exch
+    << /size 4096 /caching 1 >> object exch
+    /engines get { /engine exch def % count uncached cached driver --
+      global 4 index engine write
+      2 index engine 16!c0ffee write
+      1 index engine 16!deadbeef write
+    } foreach exch verify exch verify
+  } 1 exch fork } 1024 exch repeat waitchildren
+  global verify
+  pop
+} bind 10 exch timeout
diff --git a/fuzz/stack.c b/fuzz/stack.c
new file mode 100644
index 0000000..9a4d1f0
--- /dev/null
+++ b/fuzz/stack.c
@@ -0,0 +1,119 @@
+#include <string.h>
+
+#include "script.h"
+
+void stack_init(struct script *ctx, struct stack *stack, long size)
+{
+	stack->ctx = ctx;
+	stack->len = 0;
+	stack->size = size;
+	stack->objects = script_alloc (ctx, size, sizeof (obj_t));
+}
+
+void
+stack_fini(struct stack *stack)
+{
+	long n;
+
+	for (n = 0; n < stack->len; n++)
+		obj_free(stack->objects[n]);
+
+	script_free(stack->ctx, stack->objects);
+}
+
+void stack_roll(struct stack *stack, long mod, long n)
+{
+	obj_t stack_copy[128];
+	obj_t *copy;
+	long last, i, len;
+
+	switch (mod) { /* special cases */
+	case 1:
+		last = stack->len - 1;
+		stack_copy[0] = stack->objects[last];
+		for (i = last; --n; i--)
+			stack->objects[i] = stack->objects[i-1];
+		stack->objects[i] = stack_copy[0];
+		return;
+
+	case -1:
+		last = stack->len - 1;
+		stack_copy[0] = stack->objects[i = last - n + 1];
+		for (; --n; i++)
+			stack->objects[i] = stack->objects[i+1];
+		stack->objects[i] = stack_copy[0];
+		return;
+	}
+
+	/* fall back to a copy */
+	if (n > sizeof(stack_copy)/sizeof(stack_copy[0]))
+		copy = script_alloc (stack->ctx, n, sizeof (obj_t));
+	else
+		copy = stack_copy;
+
+	i = stack->len - n;
+	memcpy (copy, stack->objects + i, n * sizeof (obj_t));
+	mod = -mod;
+	if (mod < 0)
+		mod += n;
+	last = mod;
+	for (len = n; n--; i++) {
+		stack->objects[i] = copy[last];
+		if (++last == len)
+			last = 0;
+	}
+
+	if (copy != stack_copy)
+		script_free (stack->ctx, copy);
+}
+
+void stack_grow(struct stack *stack, long cnt)
+{
+	if (cnt <= stack->size)
+		return;
+
+	do {
+		stack->size *= 2;
+	} while (stack->size <= cnt);
+
+	stack->objects = script_realloc(stack->ctx,
+					stack->objects,
+					stack->size, sizeof (obj_t));
+}
+
+void stack_push_internal(struct stack *stack, obj_t obj)
+{
+	stack_grow(stack, stack->size + 1);
+	stack->objects[stack->len++] = obj;
+}
+
+obj_t stack_peek(struct stack *stack, long i)
+{
+	if (stack->len < i)
+		longjmp(stack->ctx->error, -EINVAL);
+
+	return stack->objects[stack->len - i -1];
+}
+
+void stack_pop(struct stack *stack, long count)
+{
+	if (stack->len < count)
+		count = stack->len;
+
+	while (count--)
+		obj_free(stack->objects[--stack->len]);
+}
+
+void stack_exch(struct stack *stack)
+{
+	obj_t tmp;
+	long n;
+
+	if (stack->len < 2)
+		longjmp(stack->ctx->error, -EINVAL);
+
+	n = stack->len - 1;
+	tmp = stack->objects[n];
+	stack->objects[n] = stack->objects[n - 1];
+	stack->objects[n - 1] = tmp;
+}
diff --git a/lib/igt_gt.c b/lib/igt_gt.c
index 95d74a0..31eae8b 100644
--- a/lib/igt_gt.c
+++ b/lib/igt_gt.c
@@ -451,7 +451,8 @@ unsigned intel_detect_and_clear_missed_interrupts(int fd)
 	unsigned missed = 0;
 	FILE *file;
 
-	gem_quiescent_gpu(fd);
+	if (fd != -1)
+		gem_quiescent_gpu(fd);
 
 	file = igt_debugfs_fopen("i915_ring_missed_irq", "r");
 	if (file) {
diff --git a/lib/igt_gt.h b/lib/igt_gt.h
index a05450d..3e3cf9c 100644
--- a/lib/igt_gt.h
+++ b/lib/igt_gt.h
@@ -73,7 +73,7 @@ extern const struct intel_execution_engine {
 	for (const struct intel_execution_engine *e__ = intel_execution_engines;\
 	     e__->name; \
 	     e__++) \
-		for_if (gem_has_ring(fd, flags__ = e__->exec_id | e__->flags))
+		for_if (gem_has_ring(fd__, flags__ = e__->exec_id | e__->flags))
 
 
 #endif /* IGT_GT_H */
-- 
2.8.1

_______________________________________________
Intel-gfx mailing list
Intel-gfx@xxxxxxxxxxxxxxxxxxxxx
https://lists.freedesktop.org/mailman/listinfo/intel-gfx





[Index of Archives]     [Linux USB Devel]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]
  Powered by Linux