diff --git a/cbits/sajson.hpp b/cbits/sajson.hpp
new file mode 100644
--- /dev/null
+++ b/cbits/sajson.hpp
@@ -0,0 +1,1722 @@
+// -*- mode: c++; c-basic-offset: 4; flycheck-clang-language-standard: "c++14"; -*-
+
+/*
+ * Copyright (c) 2012-2017 Chad Austin
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include <assert.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <string.h>
+#include <math.h>
+#include <limits.h>
+#include <algorithm>
+#include <cstdio>
+#include <limits>
+
+#ifndef SAJSON_NO_STD_STRING
+#include <string> // for convenient access to error messages and string values.
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+#define SAJSON_LIKELY(x) __builtin_expect(!!(x), 1)
+#define SAJSON_UNLIKELY(x) __builtin_expect(!!(x), 0)
+#define SAJSON_ALWAYS_INLINE __attribute__((always_inline))
+#define SAJSON_UNREACHABLE() __builtin_unreachable()
+#else
+#define SAJSON_LIKELY(x) x
+#define SAJSON_UNLIKELY(x) x
+#define SAJSON_ALWAYS_INLINE __forceinline
+#define SAJSON_UNREACHABLE() assert(!"unreachable")
+#endif
+
+namespace sajson {
+    namespace internal {
+        // This template utilizes the One Definition Rule to create global arrays in a header.
+        // This trick courtesy of Rich Geldreich's Purple JSON parser.
+        template<typename unused=void>
+        struct globals_struct {
+            static const unsigned char parse_flags[256];
+        };
+        typedef globals_struct<> globals;
+
+        // bit 0 (1) - set if: plain ASCII string character
+        // bit 1 (2) - set if: whitespace
+        // bit 4 (0x10) - set if: 0-9 e E .
+        template<typename unused>
+        const uint8_t globals_struct<unused>::parse_flags[256] = {
+         // 0    1    2    3    4    5    6    7      8    9    A    B    C    D    E    F
+            0,   0,   0,   0,   0,   0,   0,   0,     0,   2,   2,   0,   0,   2,   0,   0, // 0
+            0,   0,   0,   0,   0,   0,   0,   0,     0,   0,   0,   0,   0,   0,   0,   0, // 1
+            3,   1,   0,   1,   1,   1,   1,   1,     1,   1,   1,   1,   1,   1,   0x11,1, // 2
+            0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,  0x11,0x11,1,   1,   1,   1,   1,   1, // 3
+            1,   1,   1,   1,   1,   0x11,1,   1,     1,   1,   1,   1,   1,   1,   1,   1, // 4
+            1,   1,   1,   1,   1,   1,   1,   1,     1,   1,   1,   1,   0,   1,   1,   1, // 5
+            1,   1,   1,   1,   1,   0x11,1,   1,     1,   1,   1,   1,   1,   1,   1,   1, // 6
+            1,   1,   1,   1,   1,   1,   1,   1,     1,   1,   1,   1,   1,   1,   1,   1, // 7
+
+         // 128-255
+            0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,
+            0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,
+            0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,
+            0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0
+        };
+
+        inline bool is_plain_string_character(char c) {
+            //return c >= 0x20 && c <= 0x7f && c != 0x22 && c != 0x5c;
+            return (globals::parse_flags[static_cast<unsigned char>(c)] & 1) != 0;
+        }
+
+        inline bool is_whitespace(char c) {
+            //return c == '\r' || c == '\n' || c == '\t' || c == ' ';
+            return (globals::parse_flags[static_cast<unsigned char>(c)] & 2) != 0;
+        }
+    }
+
+    enum type: uint8_t {
+        TYPE_INTEGER = 0,
+        TYPE_DOUBLE = 1,
+        TYPE_NULL = 2,
+        TYPE_FALSE = 3,
+        TYPE_TRUE = 4,
+        TYPE_STRING = 5,
+        TYPE_ARRAY = 6,
+        TYPE_OBJECT = 7,
+    };
+
+    static const size_t TYPE_BITS = 3;
+    static const size_t TYPE_MASK = (1 << TYPE_BITS) - 1;
+    static const size_t VALUE_MASK = size_t(-1) >> TYPE_BITS;
+
+    static const size_t ROOT_MARKER = VALUE_MASK;
+
+    inline type get_element_type(size_t s) {
+        return static_cast<type>(s & TYPE_MASK);
+    }
+
+    inline size_t get_element_value(size_t s) {
+        return s >> TYPE_BITS;
+    }
+
+    inline size_t make_element(type t, size_t value) {
+        //assert((value & ~VALUE_MASK) == 0);
+        //value &= VALUE_MASK;
+        return static_cast<size_t>(t) | (value << TYPE_BITS);
+    }
+
+    class string {
+    public:
+        string(const char* text, size_t length)
+            : text(text)
+            , _length(length)
+        {}
+
+        const char* data() const {
+            return text;
+        }
+
+        size_t length() const {
+            return _length;
+        }
+
+#ifndef SAJSON_NO_STD_STRING
+        std::string as_string() const {
+            return std::string(text, text + _length);
+        }
+#endif
+
+    private:
+        const char* const text;
+        const size_t _length;
+
+        string(); /*=delete*/
+    };
+
+    class literal : public string {
+    public:
+        explicit literal(const char* text)
+            : string(text, strlen(text))
+        {}
+    };
+
+    struct object_key_record {
+        size_t key_start;
+        size_t key_end;
+        size_t value;
+    };
+
+    struct object_key_comparator {
+        object_key_comparator(const char* object_data)
+            : data(object_data)
+        {}
+
+        bool operator()(const object_key_record& lhs, const string& rhs) const {
+            const size_t lhs_length = lhs.key_end - lhs.key_start;
+            const size_t rhs_length = rhs.length();
+            if (lhs_length < rhs_length) {
+                return true;
+            } else if (lhs_length > rhs_length) {
+                return false;
+            }
+            return memcmp(data + lhs.key_start, rhs.data(), lhs_length) < 0;
+        }
+
+        bool operator()(const string& lhs, const object_key_record& rhs) const {
+            return !(*this)(rhs, lhs);
+        }
+
+        bool operator()(const object_key_record& lhs, const
+                object_key_record& rhs)
+        {
+            const size_t lhs_length = lhs.key_end - lhs.key_start;
+            const size_t rhs_length = rhs.key_end - rhs.key_start;
+            if (lhs_length < rhs_length) {
+                return true;
+            } else if (lhs_length > rhs_length) {
+                return false;
+            }
+            return memcmp(data + lhs.key_start, data + rhs.key_start,
+                    lhs_length) < 0;
+        }
+
+        const char* data;
+    };
+
+    class refcount {
+    public:
+        refcount()
+            : pn(new size_t(1))
+        {}
+
+        refcount(const refcount& rc)
+            : pn(rc.pn)
+        {
+            ++*pn;
+        }
+
+        ~refcount() {
+            if (--*pn == 0) {
+                delete pn;
+            }
+        }
+
+        size_t count() const {
+            return *pn;
+        }
+
+    private:
+        size_t* pn;
+
+        refcount& operator=(const refcount&) = delete;
+    };
+
+    class mutable_string_view {
+    public:
+        mutable_string_view()
+            : length_(0)
+            , data(0)
+        {}
+
+        mutable_string_view(size_t length, char* data)
+            : length_(length)
+            , data(data)
+        {}
+
+        size_t length() const {
+            return length_;
+        }
+
+        char* get_data() const {
+            return data;
+        }
+
+    private:
+        size_t length_;
+        char* data;
+    };
+
+    union integer_storage {
+        enum {
+            word_length = 1
+        };
+
+        static void store(size_t* location, int value) {
+            integer_storage is;
+            is.i = value;
+            *location = is.u;
+        }
+
+        int i;
+        size_t u;
+    };
+    static_assert(sizeof(integer_storage) == sizeof(size_t), "integer_storage must have same size as one structure slot");
+
+    union double_storage {
+        enum {
+            word_length = sizeof(double) / sizeof(size_t)
+        };
+
+#if defined(_M_IX86) || defined(__i386__) || defined(_X86_)
+        static double load(const size_t* location) {
+            return *reinterpret_cast<const double*>(location);
+        }
+        static void store(size_t* location, double value) {
+            *reinterpret_cast<double*>(location) = value;
+        }
+#else
+        static double load(const size_t* location) {
+            double_storage s;
+            for (unsigned i = 0; i < double_storage::word_length; ++i) {
+                s.u[i] = location[i];
+            }
+            return s.d;
+        }
+
+        static void store(size_t* location, double value) {
+            double_storage ns;
+            ns.d = value;
+
+            for (int i = 0; i < ns.word_length; ++i) {
+                location[i] = ns.u[i];
+            }
+        }
+
+        double d;
+        size_t u[word_length];
+#endif
+    };
+    // TODO: reinstate with c++03 implementation
+    //static_assert(sizeof(double_storage) == sizeof(double), "double_storage should have same size as double");
+
+    class value {
+    public:
+        explicit value(type value_type, const size_t* payload, const char* text)
+            : value_type(value_type)
+            , payload(payload)
+            , text(text)
+        {}
+
+        type get_type() const {
+            return value_type;
+        }
+
+        // valid iff get_type() is TYPE_ARRAY or TYPE_OBJECT
+        size_t get_length() const {
+            assert_type_2(TYPE_ARRAY, TYPE_OBJECT);
+            return payload[0];
+        }
+
+        // valid iff get_type() is TYPE_ARRAY
+        value get_array_element(size_t index) const {
+            assert_type(TYPE_ARRAY);
+            size_t element = payload[1 + index];
+            return value(get_element_type(element), payload + get_element_value(element), text);
+        }
+
+        // valid iff get_type() is TYPE_OBJECT
+        string get_object_key(size_t index) const {
+            assert_type(TYPE_OBJECT);
+            const size_t* s = payload + 1 + index * 3;
+            return string(text + s[0], s[1] - s[0]);
+        }
+
+        // valid iff get_type() is TYPE_OBJECT
+        value get_object_value(size_t index) const {
+            assert_type(TYPE_OBJECT);
+            size_t element = payload[3 + index * 3];
+            return value(get_element_type(element), payload + get_element_value(element), text);
+        }
+
+        // valid iff get_type() is TYPE_OBJECT
+        value get_value_of_key(const string& key) const {
+            assert_type(TYPE_OBJECT);
+            size_t i = find_object_key(key);
+            assert_in_bounds(i);
+            return get_object_value(i);
+        }
+
+        // valid iff get_type() is TYPE_OBJECT
+        // return get_length() if there is no such key
+        size_t find_object_key(const string& key) const {
+            assert_type(TYPE_OBJECT);
+            const object_key_record* start = reinterpret_cast<const object_key_record*>(payload + 1);
+            const object_key_record* end = start + get_length();
+            const object_key_record* i = std::lower_bound(start, end, key, object_key_comparator(text));
+            return (i != end
+                    && (i->key_end - i->key_start) == key.length()
+                    && memcmp(key.data(), text + i->key_start, key.length()) == 0)? i - start : get_length();
+        }
+
+        // valid iff get_type() is TYPE_INTEGER
+        int get_integer_value() const {
+            assert_type(TYPE_INTEGER);
+            integer_storage s;
+            s.u = payload[0];
+            return s.i;
+        }
+
+        // valid iff get_type() is TYPE_DOUBLE
+        double get_double_value() const {
+            assert_type(TYPE_DOUBLE);
+            return double_storage::load(payload);
+        }
+
+        // valid iff get_type() is TYPE_INTEGER or TYPE_DOUBLE
+        double get_number_value() const {
+            assert_type_2(TYPE_INTEGER, TYPE_DOUBLE);
+            if (get_type() == TYPE_INTEGER) {
+                return get_integer_value();
+            } else {
+                return get_double_value();
+            }
+        }
+
+        // valid iff get_type() is TYPE_INTEGER or TYPE_DOUBLE
+        // returns true if out is modified written.
+        // returns false if the value is a non-integral double
+        // or out of range of a 53-bit integer.
+        bool get_int53_value(int64_t* out) const {
+            // Make sure the output variable is always defined to avoid any
+            // possible situation like
+            // https://gist.github.com/chadaustin/2c249cb850619ddec05b23ca42cf7a18
+            *out = 0;
+
+            assert_type_2(TYPE_INTEGER, TYPE_DOUBLE);
+            if (get_type() == TYPE_INTEGER) {
+                *out = get_integer_value();
+                return true;
+            } else if (get_type() == TYPE_DOUBLE) {
+                double v = get_double_value();
+                if (v < -(1LL << 53) || v > (1LL << 53)) {
+                    return false;
+                }
+                int64_t as_int = static_cast<int64_t>(v);
+                if (as_int != v) {
+                    return false;
+                }
+                *out = as_int;
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        // valid iff get_type() is TYPE_STRING
+        size_t get_string_length() const {
+            assert_type(TYPE_STRING);
+            return payload[1] - payload[0];
+        }
+
+        // valid iff get_type() is TYPE_STRING
+        // WARNING: calling this function and using the return value as a
+        // C-style string (that is, without also using get_string_length())
+        // will cause the string to appear truncated if the string has
+        // embedded NULs.
+        const char* as_cstring() const {
+            assert_type(TYPE_STRING);
+            return text + payload[0];
+        }
+
+#ifndef SAJSON_NO_STD_STRING
+        // valid iff get_type() is TYPE_STRING
+        std::string as_string() const {
+            assert_type(TYPE_STRING);
+            return std::string(text + payload[0], text + payload[1]);
+        }
+#endif
+
+        const size_t* _internal_get_payload() const {
+            return payload;
+        }
+
+
+    private:
+        void assert_type(type expected) const {
+            assert(expected == get_type());
+        }
+
+        void assert_type_2(type e1, type e2) const {
+            assert(e1 == get_type() || e2 == get_type());
+        }
+
+        void assert_in_bounds(size_t i) const {
+            assert(i < get_length());
+        }
+
+        const type value_type;
+        const size_t* const payload;
+        const char* const text;
+    };
+
+    enum error {
+        ERROR_SUCCESS,
+        ERROR_OUT_OF_MEMORY,
+        ERROR_UNEXPECTED_END,
+        ERROR_MISSING_ROOT_ELEMENT,
+        ERROR_BAD_ROOT,
+        ERROR_EXPECTED_COMMA,
+        ERROR_MISSING_OBJECT_KEY,
+        ERROR_EXPECTED_COLON,
+        ERROR_EXPECTED_END_OF_INPUT,
+        ERROR_UNEXPECTED_COMMA,
+        ERROR_EXPECTED_VALUE,
+        ERROR_EXPECTED_NULL,
+        ERROR_EXPECTED_FALSE,
+        ERROR_EXPECTED_TRUE,
+        ERROR_MSSING_EXPONENT,
+        ERROR_ILLEGAL_CODEPOINT,
+        ERROR_INVALID_UNICODE_ESCAPE,
+        ERROR_UNEXPECTED_END_OF_UTF16,
+        ERROR_EXPECTED_U,
+        ERROR_INVALID_UTF16_TRAIL_SURROGATE,
+        ERROR_UNKNOWN_ESCAPE,
+        ERROR_INVALID_UTF8,
+    };
+
+    class document {
+    public:
+        explicit document(const mutable_string_view& input, type root_type, const size_t* root)
+            : input(input)
+            , root_type(root_type)
+            , root(root)
+            , error_line(0)
+            , error_column(0)
+            , error_code(ERROR_SUCCESS)
+            , error_arg(0)
+        {
+            formatted_error_message[0] = 0;
+        }
+
+        explicit document(const mutable_string_view& input, size_t error_line, size_t error_column, const error error_code, int error_arg)
+            : input(input)
+            , root_type(TYPE_NULL)
+            , root(0)
+            , error_line(error_line)
+            , error_column(error_column)
+            , error_code(error_code)
+            , error_arg(error_arg)
+        {
+            formatted_error_message[ERROR_BUFFER_LENGTH - 1] = 0;
+            int written = has_significant_error_arg()
+                ? snprintf(formatted_error_message, ERROR_BUFFER_LENGTH - 1, "%s: %d", _internal_get_error_text(), error_arg)
+                : snprintf(formatted_error_message, ERROR_BUFFER_LENGTH - 1, "%s", _internal_get_error_text());
+            (void)written;
+            assert(written >= 0 && written < ERROR_BUFFER_LENGTH);
+        }
+
+        document(const document&) = delete;
+        void operator=(const document&) = delete;
+
+        document(document&& rhs)
+            : input(rhs.input)
+            , root_type(rhs.root_type)
+            , root(rhs.root)
+            , error_line(rhs.error_line)
+            , error_column(rhs.error_column)
+            , error_code(rhs.error_code)
+            , error_arg(rhs.error_arg)
+        {
+            // Yikes... but strcpy is okay here because formatted_error is
+            // guaranteed to be null-terminated.
+            strcpy(formatted_error_message, rhs.formatted_error_message);
+            // should rhs's fields be zeroed too?
+        }
+
+        bool is_valid() const {
+            return root_type == TYPE_ARRAY || root_type == TYPE_OBJECT;
+        }
+
+        value get_root() const {
+            return value(root_type, root, input.get_data());
+        }
+
+        size_t get_error_line() const {
+            return error_line;
+        }
+
+        size_t get_error_column() const {
+            return error_column;
+        }
+
+#ifndef SAJSON_NO_STD_STRING
+        std::string get_error_message_as_string() const {
+            return formatted_error_message;
+        }
+#endif
+
+        const char* get_error_message_as_cstring() const {
+            return formatted_error_message;
+        }
+
+        /// WARNING: Internal function which is subject to change
+        error _internal_get_error_code() const {
+            return error_code;
+        }
+
+        /// WARNING: Internal function which is subject to change
+        int _internal_get_error_argument() const {
+            return error_arg;
+        }
+
+        /// WARNING: Internal function which is subject to change
+        const char* _internal_get_error_text() const {
+            switch (error_code) {
+                case ERROR_SUCCESS: return "no error";
+                case ERROR_OUT_OF_MEMORY: return  "out of memory";
+                case ERROR_UNEXPECTED_END: return  "unexpected end of input";
+                case ERROR_MISSING_ROOT_ELEMENT: return  "missing root element";
+                case ERROR_BAD_ROOT: return  "document root must be object or array";
+                case ERROR_EXPECTED_COMMA: return  "expected ,";
+                case ERROR_MISSING_OBJECT_KEY: return  "missing object key";
+                case ERROR_EXPECTED_COLON: return  "expected :";
+                case ERROR_EXPECTED_END_OF_INPUT: return  "expected end of input";
+                case ERROR_UNEXPECTED_COMMA: return  "unexpected comma";
+                case ERROR_EXPECTED_VALUE: return  "expected value";
+                case ERROR_EXPECTED_NULL: return  "expected 'null'";
+                case ERROR_EXPECTED_FALSE: return  "expected 'false'";
+                case ERROR_EXPECTED_TRUE: return  "expected 'true'";
+                case ERROR_MSSING_EXPONENT: return  "missing exponent";
+                case ERROR_ILLEGAL_CODEPOINT: return  "illegal unprintable codepoint in string";
+                case ERROR_INVALID_UNICODE_ESCAPE: return  "invalid character in unicode escape";
+                case ERROR_UNEXPECTED_END_OF_UTF16: return  "unexpected end of input during UTF-16 surrogate pair";
+                case ERROR_EXPECTED_U: return  "expected \\u";
+                case ERROR_INVALID_UTF16_TRAIL_SURROGATE: return  "invalid UTF-16 trail surrogate";
+                case ERROR_UNKNOWN_ESCAPE: return  "unknown escape";
+                case ERROR_INVALID_UTF8: return  "invalid UTF-8";
+            }
+
+            SAJSON_UNREACHABLE();
+        }
+
+        /// WARNING: Internal function exposed only for high-performance language bindings.
+        type _internal_get_root_type() const {
+            return root_type;
+        }
+
+        /// WARNING: Internal function exposed only for high-performance language bindings.
+        const size_t* _internal_get_root() const {
+            return root;
+        }
+
+        const mutable_string_view& _internal_get_input() const {
+            return input;
+        }
+
+    private:
+        bool has_significant_error_arg() const {
+            return error_code == ERROR_ILLEGAL_CODEPOINT;
+        }
+
+        mutable_string_view input;
+        const type root_type;
+        const size_t* const root;
+        const size_t error_line;
+        const size_t error_column;
+        const error error_code;
+        const int error_arg;
+
+        enum { ERROR_BUFFER_LENGTH = 128 };
+        char formatted_error_message[ERROR_BUFFER_LENGTH];
+    };
+
+    class single_allocation {
+    public:
+        class stack_head {
+        public:
+            stack_head(stack_head&& other)
+                : stack_bottom(other.stack_bottom)
+                , stack_top(other.stack_top)
+            {}
+
+            bool has_allocation_error() {
+                return false;
+            }
+
+            // check has_allocation_error() immediately after calling
+            void push(size_t element) {
+                *stack_top++ = element;
+            }
+
+            // check has_allocation_error() immediately after calling
+            size_t* reserve(size_t amount) {
+                size_t* rv = stack_top;
+                stack_top += amount;
+                return rv;
+            }
+
+            // The compiler does not see the stack_head (stored in a local)
+            // and the allocator (stored as a field) have the same stack_bottom
+            // values, so it does a bit of redundant work.
+            // So there's a microoptimization available here: introduce a type
+            // "stack_mark" and make it polymorphic on the allocator.  For
+            // single_allocation, it merely needs to be a single pointer.
+
+            void reset(size_t new_top) {
+                stack_top = stack_bottom + new_top;
+            }
+
+            size_t get_size() {
+                return stack_top - stack_bottom;
+            }
+
+            size_t* get_top() {
+                return stack_top;
+            }
+
+            size_t* get_pointer_from_offset(size_t offset) {
+                return stack_bottom + offset;
+            }
+
+        private:
+            stack_head() = delete;
+            stack_head(const stack_head&) = delete;
+            void operator=(const stack_head&) = delete;
+
+            explicit stack_head(size_t* base)
+                : stack_bottom(base)
+                , stack_top(base)
+            {}
+
+            size_t* const stack_bottom;
+            size_t* stack_top;
+
+            friend class single_allocation;
+        };
+
+        class allocator {
+        public:
+            allocator() = delete;
+            allocator(const allocator&) = delete;
+            void operator=(const allocator&) = delete;
+
+            explicit allocator(size_t* buffer, size_t input_size)
+                : structure(buffer)
+                , structure_end(buffer ? buffer + input_size : 0)
+                , write_cursor(structure_end)
+            {}
+
+            explicit allocator(std::nullptr_t)
+                : structure(0)
+                , structure_end(0)
+                , write_cursor(0)
+            {}
+
+            allocator(allocator&& other)
+                : structure(other.structure)
+                , structure_end(other.structure_end)
+                , write_cursor(other.write_cursor)
+            {
+                other.structure = 0;
+                other.structure_end = 0;
+                other.write_cursor = 0;
+            }
+
+            ~allocator() = default;
+
+            stack_head get_stack_head() {
+                return stack_head(structure);
+            }
+
+            size_t get_write_offset() {
+                return structure_end - write_cursor;
+            }
+
+            size_t* get_write_pointer_of(size_t v) {
+                return structure_end - v;
+            }
+
+            bool has_allocation_error() {
+                return false;
+            }
+
+            // check has_allocation_error immediately after calling
+            size_t* reserve(size_t size) {
+                write_cursor -= size;
+                return write_cursor;
+            }
+
+            size_t* get_ast_root() {
+                return write_cursor;
+            }
+
+            void transfer_ownership() {
+                structure = 0;
+                structure_end = 0;
+                write_cursor = 0;
+            }
+
+        private:
+            size_t* structure;
+            size_t* structure_end;
+            size_t* write_cursor;
+        };
+
+        /// Allocate a single worst-case AST buffer with one word per byte in
+        /// the input document.
+        single_allocation() = delete;
+
+
+        /// Write the AST into an existing buffer.  Will fail with an out of
+        /// memory error if the buffer is not guaranteed to be big enough for
+        /// the document.
+        single_allocation(size_t* existing_buffer, size_t size_in_words)
+            : existing_buffer(existing_buffer)
+            , existing_buffer_size(size_in_words)
+        {}
+
+        allocator make_allocator(size_t input_document_size_in_bytes, bool* succeeded) const {
+            {
+                if (existing_buffer_size < input_document_size_in_bytes) {
+                    *succeeded = false;
+                    return allocator(nullptr);
+                }
+                *succeeded = true;
+                return allocator(existing_buffer, input_document_size_in_bytes);
+            }
+        }
+
+    private:
+        size_t* existing_buffer;
+        size_t existing_buffer_size;
+    };
+
+    template<typename Allocator>
+    class parser {
+    public:
+        parser(const mutable_string_view& msv, Allocator&& allocator)
+            : input(msv)
+            , input_end(input.get_data() + input.length())
+            , allocator(std::move(allocator))
+            , root_type(TYPE_NULL)
+            , error_line(0)
+            , error_column(0)
+        {}
+
+        document get_document() {
+            if (parse()) {
+                size_t* ast_root = allocator.get_ast_root();
+                allocator.transfer_ownership();
+                return document(input, root_type, ast_root);
+            } else {
+                return document(input, error_line, error_column, error_code, error_arg);
+            }
+        }
+
+    private:
+        struct error_result {
+            operator bool() const {
+                return false;
+            }
+            operator char*() const {
+                return 0;
+            }
+        };
+
+        bool at_eof(const char* p) {
+            return p == input_end;
+        }
+
+        char* skip_whitespace(char* p) {
+            // There is an opportunity to make better use of superscalar
+            // hardware here* but if someone cares about JSON parsing
+            // performance the first thing they do is minify, so prefer
+            // to optimize for code size here.
+            // * https://github.com/chadaustin/Web-Benchmarks/blob/master/json/third-party/pjson/pjson.h#L1873
+            for (;;) {
+                if (SAJSON_UNLIKELY(p == input_end)) {
+                    return 0;
+                } else if (internal::is_whitespace(*p)) {
+                    ++p;
+                } else {
+                    return p;
+                }
+            }
+        }
+
+        error_result oom(char* p) {
+            return make_error(p, ERROR_OUT_OF_MEMORY);
+        }
+
+        error_result unexpected_end() {
+            return make_error(0, ERROR_UNEXPECTED_END);
+        }
+
+        error_result unexpected_end(char* p) {
+            return make_error(p, ERROR_UNEXPECTED_END);
+        }
+
+        error_result make_error(char* p, error code, int arg = 0) {
+            if (!p) {
+                p = input_end;
+            }
+
+            error_line = 1;
+            error_column = 1;
+
+            char* c = input.get_data();
+            while (c < p) {
+                if (*c == '\r') {
+                    if (c + 1 < p && c[1] == '\n') {
+                        ++error_line;
+                        error_column = 1;
+                        ++c;
+                    } else {
+                        ++error_line;
+                        error_column = 1;
+                    }
+                } else if (*c == '\n') {
+                    ++error_line;
+                    error_column = 1;
+                } else {
+                    // TODO: count UTF-8 characters
+                    ++error_column;
+                }
+                ++c;
+            }
+
+            error_code = code;
+            error_arg = arg;
+            return error_result();
+        }
+
+        bool parse() {
+            // p points to the character currently being parsed
+            char* p = input.get_data();
+
+            auto stack = allocator.get_stack_head();
+            if (SAJSON_UNLIKELY(stack.has_allocation_error())) {
+                return oom(p);
+            }
+
+            p = skip_whitespace(p);
+            if (SAJSON_UNLIKELY(!p)) {
+                return make_error(p, ERROR_MISSING_ROOT_ELEMENT);
+            }
+
+            // current_base is an offset to the first element of the current structure (object or array)
+            size_t current_base = stack.get_size();
+            type current_structure_type;
+            if (*p == '[') {
+                current_structure_type = TYPE_ARRAY;
+                stack.push(make_element(current_structure_type, ROOT_MARKER));
+                if (SAJSON_UNLIKELY(stack.has_allocation_error())) {
+                    return oom(p);
+                }
+                goto array_close_or_element;
+            } else if (*p == '{') {
+                current_structure_type = TYPE_OBJECT;
+                stack.push(make_element(current_structure_type, ROOT_MARKER));
+                if (SAJSON_UNLIKELY(stack.has_allocation_error())) {
+                    return oom(p);
+                }
+                goto object_close_or_element;
+            } else {
+                return make_error(p, ERROR_BAD_ROOT);
+            }
+
+            // BEGIN STATE MACHINE
+
+            size_t pop_element; // used as an argument into the `pop` routine
+
+            if (0) { // purely for structure
+
+            // ASSUMES: byte at p SHOULD be skipped
+            array_close_or_element:
+                p = skip_whitespace(p + 1);
+                if (SAJSON_UNLIKELY(!p)) {
+                    return unexpected_end();
+                }
+                if (*p == ']') {
+                    goto pop_array;
+                } else {
+                    goto next_element;
+                }
+                SAJSON_UNREACHABLE();
+
+            // ASSUMES: byte at p SHOULD be skipped
+            object_close_or_element:
+                p = skip_whitespace(p + 1);
+                if (SAJSON_UNLIKELY(!p)) {
+                    return unexpected_end();
+                }
+                if (*p == '}') {
+                    goto pop_object;
+                } else {
+                    goto object_key;
+                }
+                SAJSON_UNREACHABLE();
+
+            // ASSUMES: byte at p SHOULD NOT be skipped
+            structure_close_or_comma:
+                p = skip_whitespace(p);
+                if (SAJSON_UNLIKELY(!p)) {
+                    return unexpected_end();
+                }
+
+                if (current_structure_type == TYPE_ARRAY) {
+                    if (*p == ']') {
+                        goto pop_array;
+                    } else {
+                        if (SAJSON_UNLIKELY(*p != ',')) {
+                            return make_error(p, ERROR_EXPECTED_COMMA);
+                        }
+                        ++p;
+                        goto next_element;
+                    }
+                } else {
+                    assert(current_structure_type == TYPE_OBJECT);
+                    if (*p == '}') {
+                        goto pop_object;
+                    } else {
+                        if (SAJSON_UNLIKELY(*p != ',')) {
+                            return make_error(p, ERROR_EXPECTED_COMMA);
+                        }
+                        ++p;
+                        goto object_key;
+                    }
+                }
+                SAJSON_UNREACHABLE();
+
+            // ASSUMES: *p == '}'
+            pop_object: {
+                ++p;
+                size_t* base_ptr = stack.get_pointer_from_offset(current_base);
+                pop_element = *base_ptr;
+                if (SAJSON_UNLIKELY(!install_object(base_ptr + 1, stack.get_top()))) {
+                    return oom(p);
+                }
+                goto pop;
+            }
+
+            // ASSUMES: *p == ']'
+            pop_array: {
+                ++p;
+                size_t* base_ptr = stack.get_pointer_from_offset(current_base);
+                pop_element = *base_ptr;
+                if (SAJSON_UNLIKELY(!install_array(base_ptr + 1, stack.get_top()))) {
+                    return oom(p);
+                }
+                goto pop;
+            }
+
+            // ASSUMES: byte at p SHOULD NOT be skipped
+            object_key: {
+                p = skip_whitespace(p);
+                if (SAJSON_UNLIKELY(!p)) {
+                    return unexpected_end();
+                }
+                if (SAJSON_UNLIKELY(*p != '"')) {
+                    return make_error(p, ERROR_MISSING_OBJECT_KEY);
+                }
+                size_t* out = stack.reserve(2);
+                if (SAJSON_UNLIKELY(stack.has_allocation_error())) {
+                    return oom(p);
+                }
+                p = parse_string(p, out);
+                if (SAJSON_UNLIKELY(!p)) {
+                    return false;
+                }
+                p = skip_whitespace(p);
+                if (SAJSON_UNLIKELY(!p || *p != ':')) {
+                    return make_error(p, ERROR_EXPECTED_COLON);
+                }
+                ++p;
+                goto next_element;
+            }
+
+            // ASSUMES: byte at p SHOULD NOT be skipped
+            next_element:
+                p = skip_whitespace(p);
+                if (SAJSON_UNLIKELY(!p)) {
+                    return unexpected_end();
+                }
+
+                type value_type_result;
+                switch (*p) {
+                    case 0:
+                        return unexpected_end(p);
+                    case 'n':
+                        p = parse_null(p);
+                        if (!p) {
+                            return false;
+                        }
+                        value_type_result = TYPE_NULL;
+                        break;
+                    case 'f':
+                        p = parse_false(p);
+                        if (!p) {
+                            return false;
+                        }
+                        value_type_result = TYPE_FALSE;
+                        break;
+                    case 't':
+                        p = parse_true(p);
+                        if (!p) {
+                            return false;
+                        }
+                        value_type_result = TYPE_TRUE;
+                        break;
+                    case '0':
+                    case '1':
+                    case '2':
+                    case '3':
+                    case '4':
+                    case '5':
+                    case '6':
+                    case '7':
+                    case '8':
+                    case '9':
+                    case '-': {
+                        auto result = parse_number(p);
+                        p = result.first;
+                        if (!p) {
+                            return false;
+                        }
+                        value_type_result = result.second;
+                        break;
+                    }
+                    case '"': {
+                        size_t* string_tag = allocator.reserve(2);
+                        if (allocator.has_allocation_error()) {
+                            return oom(p);
+                        }
+                        p = parse_string(p, string_tag);
+                        if (!p) {
+                            return false;
+                        }
+                        value_type_result = TYPE_STRING;
+                        break;
+                    }
+
+                    case '[': {
+                        size_t previous_base = current_base;
+                        current_base = stack.get_size();
+                        stack.push(make_element(current_structure_type, previous_base));
+                        if (stack.has_allocation_error()) {
+                            return oom(p);
+                        }
+                        current_structure_type = TYPE_ARRAY;
+                        goto array_close_or_element;
+                    }
+                    case '{': {
+                        size_t previous_base = current_base;
+                        current_base = stack.get_size();
+                        stack.push(make_element(current_structure_type, previous_base));
+                        if (stack.has_allocation_error()) {
+                            return oom(p);
+                        }
+                        current_structure_type = TYPE_OBJECT;
+                        goto object_close_or_element;
+                    }
+                    pop: {
+                        size_t parent = get_element_value(pop_element);
+                        if (parent == ROOT_MARKER) {
+                            root_type = current_structure_type;
+                            p = skip_whitespace(p);
+                            if (SAJSON_UNLIKELY(p)) {
+                                return make_error(p, ERROR_EXPECTED_END_OF_INPUT);
+                            }
+                            return true;
+                        }
+                        stack.reset(current_base);
+                        current_base = parent;
+                        value_type_result = current_structure_type;
+                        current_structure_type = get_element_type(pop_element);
+                        break;
+                    }
+
+                    case ',':
+                        return make_error(p, ERROR_UNEXPECTED_COMMA);
+                    default:
+                        return make_error(p, ERROR_EXPECTED_VALUE);
+                }
+
+                stack.push(make_element(value_type_result, allocator.get_write_offset()));
+                if (SAJSON_UNLIKELY(stack.has_allocation_error())) {
+                    return oom(p);
+                }
+
+                goto structure_close_or_comma;
+            }
+
+            SAJSON_UNREACHABLE();
+        }
+
+        bool has_remaining_characters(char* p, ptrdiff_t remaining) {
+            return input_end - p >= remaining;
+        }
+
+        char* parse_null(char* p) {
+            if (SAJSON_UNLIKELY(!has_remaining_characters(p, 4))) {
+                make_error(p, ERROR_UNEXPECTED_END);
+                return 0;
+            }
+            char p1 = p[1];
+            char p2 = p[2];
+            char p3 = p[3];
+            if (SAJSON_UNLIKELY(p1 != 'u' || p2 != 'l' || p3 != 'l')) {
+                make_error(p, ERROR_EXPECTED_NULL);
+                return 0;
+            }
+            return p + 4;
+        }
+
+        char* parse_false(char* p) {
+            if (SAJSON_UNLIKELY(!has_remaining_characters(p, 5))) {
+                return make_error(p, ERROR_UNEXPECTED_END);
+            }
+            char p1 = p[1];
+            char p2 = p[2];
+            char p3 = p[3];
+            char p4 = p[4];
+            if (SAJSON_UNLIKELY(p1 != 'a' || p2 != 'l' || p3 != 's' || p4 != 'e')) {
+                return make_error(p, ERROR_EXPECTED_FALSE);
+            }
+            return p + 5;
+        }
+
+        char* parse_true(char* p) {
+            if (SAJSON_UNLIKELY(!has_remaining_characters(p, 4))) {
+                return make_error(p, ERROR_UNEXPECTED_END);
+            }
+            char p1 = p[1];
+            char p2 = p[2];
+            char p3 = p[3];
+            if (SAJSON_UNLIKELY(p1 != 'r' || p2 != 'u' || p3 != 'e')) {
+                return make_error(p, ERROR_EXPECTED_TRUE);
+            }
+            return p + 4;
+        }
+
+        static double pow10(int exponent) {
+            if (exponent > 308) {
+                return std::numeric_limits<double>::infinity();
+            } else if (exponent < -323) {
+                return 0.0;
+            }
+            static const double constants[] = {
+                1e-323,1e-322,1e-321,1e-320,1e-319,1e-318,1e-317,1e-316,1e-315,1e-314,
+                1e-313,1e-312,1e-311,1e-310,1e-309,1e-308,1e-307,1e-306,1e-305,1e-304,
+                1e-303,1e-302,1e-301,1e-300,1e-299,1e-298,1e-297,1e-296,1e-295,1e-294,
+                1e-293,1e-292,1e-291,1e-290,1e-289,1e-288,1e-287,1e-286,1e-285,1e-284,
+                1e-283,1e-282,1e-281,1e-280,1e-279,1e-278,1e-277,1e-276,1e-275,1e-274,
+                1e-273,1e-272,1e-271,1e-270,1e-269,1e-268,1e-267,1e-266,1e-265,1e-264,
+                1e-263,1e-262,1e-261,1e-260,1e-259,1e-258,1e-257,1e-256,1e-255,1e-254,
+                1e-253,1e-252,1e-251,1e-250,1e-249,1e-248,1e-247,1e-246,1e-245,1e-244,
+                1e-243,1e-242,1e-241,1e-240,1e-239,1e-238,1e-237,1e-236,1e-235,1e-234,
+                1e-233,1e-232,1e-231,1e-230,1e-229,1e-228,1e-227,1e-226,1e-225,1e-224,
+                1e-223,1e-222,1e-221,1e-220,1e-219,1e-218,1e-217,1e-216,1e-215,1e-214,
+                1e-213,1e-212,1e-211,1e-210,1e-209,1e-208,1e-207,1e-206,1e-205,1e-204,
+                1e-203,1e-202,1e-201,1e-200,1e-199,1e-198,1e-197,1e-196,1e-195,1e-194,
+                1e-193,1e-192,1e-191,1e-190,1e-189,1e-188,1e-187,1e-186,1e-185,1e-184,
+                1e-183,1e-182,1e-181,1e-180,1e-179,1e-178,1e-177,1e-176,1e-175,1e-174,
+                1e-173,1e-172,1e-171,1e-170,1e-169,1e-168,1e-167,1e-166,1e-165,1e-164,
+                1e-163,1e-162,1e-161,1e-160,1e-159,1e-158,1e-157,1e-156,1e-155,1e-154,
+                1e-153,1e-152,1e-151,1e-150,1e-149,1e-148,1e-147,1e-146,1e-145,1e-144,
+                1e-143,1e-142,1e-141,1e-140,1e-139,1e-138,1e-137,1e-136,1e-135,1e-134,
+                1e-133,1e-132,1e-131,1e-130,1e-129,1e-128,1e-127,1e-126,1e-125,1e-124,
+                1e-123,1e-122,1e-121,1e-120,1e-119,1e-118,1e-117,1e-116,1e-115,1e-114,
+                1e-113,1e-112,1e-111,1e-110,1e-109,1e-108,1e-107,1e-106,1e-105,1e-104,
+                1e-103,1e-102,1e-101,1e-100,1e-99,1e-98,1e-97,1e-96,1e-95,1e-94,1e-93,
+                1e-92,1e-91,1e-90,1e-89,1e-88,1e-87,1e-86,1e-85,1e-84,1e-83,1e-82,1e-81,
+                1e-80,1e-79,1e-78,1e-77,1e-76,1e-75,1e-74,1e-73,1e-72,1e-71,1e-70,1e-69,
+                1e-68,1e-67,1e-66,1e-65,1e-64,1e-63,1e-62,1e-61,1e-60,1e-59,1e-58,1e-57,
+                1e-56,1e-55,1e-54,1e-53,1e-52,1e-51,1e-50,1e-49,1e-48,1e-47,1e-46,1e-45,
+                1e-44,1e-43,1e-42,1e-41,1e-40,1e-39,1e-38,1e-37,1e-36,1e-35,1e-34,1e-33,
+                1e-32,1e-31,1e-30,1e-29,1e-28,1e-27,1e-26,1e-25,1e-24,1e-23,1e-22,1e-21,
+                1e-20,1e-19,1e-18,1e-17,1e-16,1e-15,1e-14,1e-13,1e-12,1e-11,1e-10,1e-9,
+                1e-8,1e-7,1e-6,1e-5,1e-4,1e-3,1e-2,1e-1,1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,
+                1e8,1e9,1e10,1e11,1e12,1e13,1e14,1e15,1e16,1e17,1e18,1e19,1e20,1e21,
+                1e22,1e23,1e24,1e25,1e26,1e27,1e28,1e29,1e30,1e31,1e32,1e33,1e34,1e35,
+                1e36,1e37,1e38,1e39,1e40,1e41,1e42,1e43,1e44,1e45,1e46,1e47,1e48,1e49,
+                1e50,1e51,1e52,1e53,1e54,1e55,1e56,1e57,1e58,1e59,1e60,1e61,1e62,1e63,
+                1e64,1e65,1e66,1e67,1e68,1e69,1e70,1e71,1e72,1e73,1e74,1e75,1e76,1e77,
+                1e78,1e79,1e80,1e81,1e82,1e83,1e84,1e85,1e86,1e87,1e88,1e89,1e90,1e91,
+                1e92,1e93,1e94,1e95,1e96,1e97,1e98,1e99,1e100,1e101,1e102,1e103,1e104,
+                1e105,1e106,1e107,1e108,1e109,1e110,1e111,1e112,1e113,1e114,1e115,1e116,
+                1e117,1e118,1e119,1e120,1e121,1e122,1e123,1e124,1e125,1e126,1e127,1e128,
+                1e129,1e130,1e131,1e132,1e133,1e134,1e135,1e136,1e137,1e138,1e139,1e140,
+                1e141,1e142,1e143,1e144,1e145,1e146,1e147,1e148,1e149,1e150,1e151,1e152,
+                1e153,1e154,1e155,1e156,1e157,1e158,1e159,1e160,1e161,1e162,1e163,1e164,
+                1e165,1e166,1e167,1e168,1e169,1e170,1e171,1e172,1e173,1e174,1e175,1e176,
+                1e177,1e178,1e179,1e180,1e181,1e182,1e183,1e184,1e185,1e186,1e187,1e188,
+                1e189,1e190,1e191,1e192,1e193,1e194,1e195,1e196,1e197,1e198,1e199,1e200,
+                1e201,1e202,1e203,1e204,1e205,1e206,1e207,1e208,1e209,1e210,1e211,1e212,
+                1e213,1e214,1e215,1e216,1e217,1e218,1e219,1e220,1e221,1e222,1e223,1e224,
+                1e225,1e226,1e227,1e228,1e229,1e230,1e231,1e232,1e233,1e234,1e235,1e236,
+                1e237,1e238,1e239,1e240,1e241,1e242,1e243,1e244,1e245,1e246,1e247,1e248,
+                1e249,1e250,1e251,1e252,1e253,1e254,1e255,1e256,1e257,1e258,1e259,1e260,
+                1e261,1e262,1e263,1e264,1e265,1e266,1e267,1e268,1e269,1e270,1e271,1e272,
+                1e273,1e274,1e275,1e276,1e277,1e278,1e279,1e280,1e281,1e282,1e283,1e284,
+                1e285,1e286,1e287,1e288,1e289,1e290,1e291,1e292,1e293,1e294,1e295,1e296,
+                1e297,1e298,1e299,1e300,1e301,1e302,1e303,1e304,1e305,1e306,1e307,1e308
+            };
+            return constants[exponent + 323];
+        }
+
+        std::pair<char*, type> parse_number(char* p) {
+            bool negative = false;
+            if ('-' == *p) {
+                ++p;
+                negative = true;
+
+                if (SAJSON_UNLIKELY(at_eof(p))) {
+                    return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                }
+            }
+
+            bool try_double = false;
+
+            int i = 0;
+            double d = 0.0; // gcc complains that d might be used uninitialized which isn't true. appease the warning anyway.
+            if (*p == '0') {
+                ++p;
+                if (SAJSON_UNLIKELY(at_eof(p))) {
+                    return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                }
+            } else for (;;) {
+                unsigned char c = *p;
+                if (c < '0' || c > '9') {
+                    break;
+                }
+
+                ++p;
+                if (SAJSON_UNLIKELY(at_eof(p))) {
+                    return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                }
+
+                unsigned char digit = c - '0';
+
+                if (SAJSON_UNLIKELY(!try_double && i > INT_MAX / 10 - 9)) {
+                    // TODO: could split this into two loops
+                    try_double = true;
+                    d = i;
+                }
+                if (SAJSON_UNLIKELY(try_double)) {
+                    d = 10.0 * d + digit;
+                } else {
+                    i = 10 * i + digit;
+                }
+            }
+
+            int exponent = 0;
+
+            if ('.' == *p) {
+                if (!try_double) {
+                    try_double = true;
+                    d = i;
+                }
+                ++p;
+                if (SAJSON_UNLIKELY(at_eof(p))) {
+                    return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                }
+                for (;;) {
+                    char c = *p;
+                    if (c < '0' || c > '9') {
+                        break;
+                    }
+
+                    ++p;
+                    if (SAJSON_UNLIKELY(at_eof(p))) {
+                        return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                    }
+                    d = d * 10 + (c - '0');
+                    --exponent;
+                }
+            }
+
+            char e = *p;
+            if ('e' == e || 'E' == e) {
+                if (!try_double) {
+                    try_double = true;
+                    d = i;
+                }
+                ++p;
+                if (SAJSON_UNLIKELY(at_eof(p))) {
+                    return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                }
+
+                bool negativeExponent = false;
+                if ('-' == *p) {
+                    negativeExponent = true;
+                    ++p;
+                    if (SAJSON_UNLIKELY(at_eof(p))) {
+                        return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                    }
+                } else if ('+' == *p) {
+                    ++p;
+                    if (SAJSON_UNLIKELY(at_eof(p))) {
+                        return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                    }
+                }
+
+                int exp = 0;
+
+                char c = *p;
+                if (SAJSON_UNLIKELY(c < '0' || c > '9')) {
+                    return std::make_pair(make_error(p, ERROR_MSSING_EXPONENT), TYPE_NULL);
+                }
+                for (;;) {
+                    exp = 10 * exp + (c - '0');
+
+                    ++p;
+                    if (SAJSON_UNLIKELY(at_eof(p))) {
+                        return std::make_pair(make_error(p, ERROR_UNEXPECTED_END), TYPE_NULL);
+                    }
+
+                    c = *p;
+                    if (c < '0' || c > '9') {
+                        break;
+                    }
+                }
+                exponent += (negativeExponent ? -exp : exp);
+            }
+
+            if (exponent) {
+                assert(try_double);
+                d *= pow10(exponent);
+            }
+
+            if (negative) {
+                if (try_double) {
+                    d = -d;
+                } else {
+                    i = -i;
+                }
+            }
+            if (try_double) {
+                size_t* out = allocator.reserve(double_storage::word_length);
+                if (allocator.has_allocation_error()) {
+                    return std::make_pair(oom(p), TYPE_NULL);
+                }
+                double_storage::store(out, d);
+                return std::make_pair(p, TYPE_DOUBLE);
+            } else {
+                size_t* out = allocator.reserve(integer_storage::word_length);
+                if (allocator.has_allocation_error()) {
+                    return std::make_pair(oom(p), TYPE_NULL);
+                }
+                integer_storage::store(out, i);
+                return std::make_pair(p, TYPE_INTEGER);
+            }
+        }
+
+        bool install_array(size_t* array_base, size_t* array_end) {
+            const size_t length = array_end - array_base;
+            size_t* const new_base = allocator.reserve(length + 1);
+            if (SAJSON_UNLIKELY(allocator.has_allocation_error())) {
+                return false;
+            }
+            size_t* out = new_base + length + 1;
+            size_t* const structure_end = allocator.get_write_pointer_of(0);
+
+            while (array_end > array_base) {
+                size_t element = *--array_end;
+                type element_type = get_element_type(element);
+                size_t element_value = get_element_value(element);
+                size_t* element_ptr = structure_end - element_value;
+                *--out = make_element(element_type, element_ptr - new_base);
+            }
+            *--out = length;
+            return true;
+        }
+
+        bool install_object(size_t* object_base, size_t* object_end) {
+            assert((object_end - object_base) % 3 == 0);
+            const size_t length_times_3 = object_end - object_base;
+            std::sort(
+                reinterpret_cast<object_key_record*>(object_base),
+                reinterpret_cast<object_key_record*>(object_end),
+                object_key_comparator(input.get_data()));
+
+            size_t* const new_base = allocator.reserve(length_times_3 + 1);
+            if (SAJSON_UNLIKELY(allocator.has_allocation_error())) {
+                return false;
+            }
+            size_t* out = new_base + length_times_3 + 1;
+            size_t* const structure_end = allocator.get_write_pointer_of(0);
+
+            while (object_end > object_base) {
+                size_t element = *--object_end;
+                type element_type = get_element_type(element);
+                size_t element_value = get_element_value(element);
+                size_t* element_ptr = structure_end - element_value;
+
+                *--out = make_element(element_type, element_ptr - new_base);
+                *--out = *--object_end;
+                *--out = *--object_end;
+            }
+            *--out = length_times_3 / 3;
+            return true;
+        }
+
+        char* parse_string(char* p, size_t* tag) {
+            ++p; // "
+            size_t start = p - input.get_data();
+            char* input_end_local = input_end;
+            while (input_end_local - p >= 4) {
+                if (!internal::is_plain_string_character(p[0])) { goto found; }
+                if (!internal::is_plain_string_character(p[1])) { p += 1; goto found; }
+                if (!internal::is_plain_string_character(p[2])) { p += 2; goto found; }
+                if (!internal::is_plain_string_character(p[3])) { p += 3; goto found; }
+                p += 4;
+            }
+            for (;;) {
+                if (SAJSON_UNLIKELY(p >= input_end_local)) {
+                    return make_error(p, ERROR_UNEXPECTED_END);
+                }
+
+                if (!internal::is_plain_string_character(*p)) {
+                    break;
+                }
+
+                ++p;
+            }
+        found:
+            if (SAJSON_LIKELY(*p == '"')) {
+                tag[0] = start;
+                tag[1] = p - input.get_data();
+                *p = '\0';
+                return p + 1;
+            }
+
+            if (*p >= 0 && *p < 0x20) {
+                return make_error(p, ERROR_ILLEGAL_CODEPOINT, static_cast<int>(*p));
+            } else {
+                // backslash or >0x7f
+                return parse_string_slow(p, tag, start);
+            }
+        }
+
+        char* read_hex(char* p, unsigned& u) {
+            unsigned v = 0;
+            int i = 4;
+            while (i--) {
+                unsigned char c = *p++;
+                if (c >= '0' && c <= '9') {
+                    c -= '0';
+                } else if (c >= 'a' && c <= 'f') {
+                    c = c - 'a' + 10;
+                } else if (c >= 'A' && c <= 'F') {
+                    c = c - 'A' + 10;
+                } else {
+                    return make_error(p, ERROR_INVALID_UNICODE_ESCAPE);
+                }
+                v = (v << 4) + c;
+            }
+
+            u = v;
+            return p;
+        }
+
+        void write_utf8(unsigned codepoint, char*& end) {
+            if (codepoint < 0x80) {
+                *end++ = codepoint;
+            } else if (codepoint < 0x800) {
+                *end++ = 0xC0 | (codepoint >> 6);
+                *end++ = 0x80 | (codepoint & 0x3F);
+            } else if (codepoint < 0x10000) {
+                *end++ = 0xE0 | (codepoint >> 12);
+                *end++ = 0x80 | ((codepoint >> 6) & 0x3F);
+                *end++ = 0x80 | (codepoint & 0x3F);
+            } else {
+                assert(codepoint < 0x200000);
+                *end++ = 0xF0 | (codepoint >> 18);
+                *end++ = 0x80 | ((codepoint >> 12) & 0x3F);
+                *end++ = 0x80 | ((codepoint >> 6) & 0x3F);
+                *end++ = 0x80 | (codepoint & 0x3F);
+            }
+        }
+
+        char* parse_string_slow(char* p, size_t* tag, size_t start) {
+            char* end = p;
+            char* input_end_local = input_end;
+
+            for (;;) {
+                if (SAJSON_UNLIKELY(p >= input_end_local)) {
+                    return make_error(p, ERROR_UNEXPECTED_END);
+                }
+
+                if (SAJSON_UNLIKELY(*p >= 0 && *p < 0x20)) {
+                    return make_error(p, ERROR_ILLEGAL_CODEPOINT, static_cast<int>(*p));
+                }
+
+                switch (*p) {
+                    case '"':
+                        tag[0] = start;
+                        tag[1] = end - input.get_data();
+                        *end = '\0';
+                        return p + 1;
+
+                    case '\\':
+                        ++p;
+                        if (SAJSON_UNLIKELY(p >= input_end_local)) {
+                            return make_error(p, ERROR_UNEXPECTED_END);
+                        }
+
+                        char replacement;
+                        switch (*p) {
+                            case '"': replacement = '"'; goto replace;
+                            case '\\': replacement = '\\'; goto replace;
+                            case '/': replacement = '/'; goto replace;
+                            case 'b': replacement = '\b'; goto replace;
+                            case 'f': replacement = '\f'; goto replace;
+                            case 'n': replacement = '\n'; goto replace;
+                            case 'r': replacement = '\r'; goto replace;
+                            case 't': replacement = '\t'; goto replace;
+                            replace:
+                                *end++ = replacement;
+                                ++p;
+                                break;
+                            case 'u': {
+                                ++p;
+                                if (SAJSON_UNLIKELY(!has_remaining_characters(p, 4))) {
+                                    return make_error(p, ERROR_UNEXPECTED_END);
+                                }
+                                unsigned u = 0; // gcc's complaining that this could be used uninitialized. wrong.
+                                p = read_hex(p, u);
+                                if (!p) {
+                                    return 0;
+                                }
+                                if (u >= 0xD800 && u <= 0xDBFF) {
+                                    if (SAJSON_UNLIKELY(!has_remaining_characters(p, 6))) {
+                                        return make_error(p, ERROR_UNEXPECTED_END_OF_UTF16);
+                                    }
+                                    char p0 = p[0];
+                                    char p1 = p[1];
+                                    if (p0 != '\\' || p1 != 'u') {
+                                        return make_error(p, ERROR_EXPECTED_U);
+                                    }
+                                    p += 2;
+                                    unsigned v = 0; // gcc's complaining that this could be used uninitialized. wrong.
+                                    p = read_hex(p, v);
+                                    if (!p) {
+                                        return p;
+                                    }
+
+                                    if (v < 0xDC00 || v > 0xDFFF) {
+                                        return make_error(p, ERROR_INVALID_UTF16_TRAIL_SURROGATE);
+                                    }
+                                    u = 0x10000 + (((u - 0xD800) << 10) | (v - 0xDC00));
+                                }
+                                write_utf8(u, end);
+                                break;
+                            }
+                            default:
+                                return make_error(p, ERROR_UNKNOWN_ESCAPE);
+                        }
+                        break;
+
+                    default:
+                        // validate UTF-8
+                        unsigned char c0 = p[0];
+                        if (c0 < 128) {
+                            *end++ = *p++;
+                        } else if (c0 < 224) {
+                            if (SAJSON_UNLIKELY(!has_remaining_characters(p, 2))) {
+                                return unexpected_end(p);
+                            }
+                            unsigned char c1 = p[1];
+                            if (c1 < 128 || c1 >= 192) {
+                                return make_error(p + 1, ERROR_INVALID_UTF8);
+                            }
+                            end[0] = c0;
+                            end[1] = c1;
+                            end += 2;
+                            p += 2;
+                        } else if (c0 < 240) {
+                            if (SAJSON_UNLIKELY(!has_remaining_characters(p, 3))) {
+                                return unexpected_end(p);
+                            }
+                            unsigned char c1 = p[1];
+                            if (c1 < 128 || c1 >= 192) {
+                                return make_error(p + 1, ERROR_INVALID_UTF8);
+                            }
+                            unsigned char c2 = p[2];
+                            if (c2 < 128 || c2 >= 192) {
+                                return make_error(p + 2, ERROR_INVALID_UTF8);
+                            }
+                            end[0] = c0;
+                            end[1] = c1;
+                            end[2] = c2;
+                            end += 3;
+                            p += 3;
+                        } else if (c0 < 248) {
+                            if (SAJSON_UNLIKELY(!has_remaining_characters(p, 4))) {
+                                return unexpected_end(p);
+                            }
+                            unsigned char c1 = p[1];
+                            if (c1 < 128 || c1 >= 192) {
+                                return make_error(p + 1, ERROR_INVALID_UTF8);
+                            }
+                            unsigned char c2 = p[2];
+                            if (c2 < 128 || c2 >= 192) {
+                                return make_error(p + 2, ERROR_INVALID_UTF8);
+                            }
+                            unsigned char c3 = p[3];
+                            if (c3 < 128 || c3 >= 192) {
+                                return make_error(p + 3, ERROR_INVALID_UTF8);
+                            }
+                            end[0] = c0;
+                            end[1] = c1;
+                            end[2] = c2;
+                            end[3] = c3;
+                            end += 4;
+                            p += 4;
+                        } else {
+                            return make_error(p, ERROR_INVALID_UTF8);
+                        }
+                        break;
+                }
+            }
+        }
+
+        mutable_string_view input;
+        char* const input_end;
+        Allocator allocator;
+
+        type root_type;
+        size_t error_line;
+        size_t error_column;
+        error error_code;
+        int error_arg; // optional argument for the error
+    };
+
+    template<typename AllocationStrategy, typename StringType>
+    document parse(const AllocationStrategy& strategy, const StringType& string) {
+        mutable_string_view input(string);
+
+        bool success;
+        auto allocator = strategy.make_allocator(input.length(), &success);
+        if (!success) {
+            return document(input, 1, 1, ERROR_OUT_OF_MEMORY, 0);
+        }
+
+        return parser<typename AllocationStrategy::allocator>(
+            input,
+            std::move(allocator)
+        ).get_document();
+    }
+}
diff --git a/cbits/sajson_wrapper.h b/cbits/sajson_wrapper.h
new file mode 100644
--- /dev/null
+++ b/cbits/sajson_wrapper.h
@@ -0,0 +1,28 @@
+#ifndef SAJSON_WRAPPER_H
+#define SAJSON_WRAPPER_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+struct sajson_document;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+size_t sajson_document_sizeof(void);
+struct sajson_document *sajson_parse_single_allocation(char *str, size_t length, size_t *buffer, char *rv);
+void sajson_free_document(struct sajson_document *doc);
+int sajson_has_error(struct sajson_document *doc);
+size_t sajson_get_error_line(struct sajson_document *doc);
+size_t sajson_get_error_column(struct sajson_document *doc);
+const char *sajson_get_error_message(struct sajson_document *doc);
+uint8_t sajson_get_root_type(struct sajson_document *doc);
+const size_t *sajson_get_root(struct sajson_document *doc);
+const unsigned char *sajson_get_input(struct sajson_document *doc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/sajson.cabal b/sajson.cabal
--- a/sajson.cabal
+++ b/sajson.cabal
@@ -1,30 +1,40 @@
-name: sajson
-version: 0.1.0.0
-cabal-version: >=1.10
-build-type: Simple
-license: MIT
-license-file: LICENSE
-copyright: (c) 2012-2017 Chad Austin
-           (c) 2017 Zhouyu Qian
-maintainer: qzy@qzy.io
-stability: experimental
-homepage: https://github.com/kccqzy/haskell-sajson#readme
-synopsis: Fast JSON parsing powered by Chad Austin's sajson library
-description:
-    A fast JSON parsing library that is faster than aeson.
-category: Web, Text, JSON
-author: Chad Austin, Zhouyu Qian
-tested-with: GHC ==8.0.2
-extra-source-files:
-    README.md
+cabal-version:      >=1.10
+name:               sajson
+version:            0.2.0.0
+license:            MIT
+license-file:       LICENSE
+copyright:
+    (c) 2012-2017 Chad Austin
+    (c) 2017 Zhouyu Qian
 
+maintainer:         qzy@qzy.io
+author:             Chad Austin, Zhouyu Qian
+stability:          experimental
+tested-with:        ghc ==8.0.2
+homepage:           https://github.com/kccqzy/haskell-sajson#readme
+synopsis:           Fast JSON parsing powered by Chad Austin's sajson library
+description:        A fast JSON parsing library that is faster than aeson.
+category:           Web, Text, JSON
+build-type:         Simple
+extra-source-files: README.md
+
 source-repository head
-    type: git
+    type:     git
     location: https://github.com/kccqzy/haskell-sajson
 
 library
-    exposed-modules:
-        Data.Sajson
+    exposed-modules:  Data.Sajson
+    cc-options:       -Wall -O3 -march=native -std=c++11 -fomit-frame-pointer
+    c-sources:
+        cbits/sajson_wrapper.cpp
+        cbits/sajson_wrapper.h
+
+    hs-source-dirs:   src
+    default-language: Haskell2010
+    extra-libraries:  stdc++
+    include-dirs:     cbits
+    install-includes: cbits/sajson.hpp
+    ghc-options:      -Wall -O2
     build-depends:
         base >=4.7 && <5,
         bytestring >=0.10.8.1 && <0.11,
@@ -33,34 +43,28 @@
         vector >=0.11.0.0 && <0.12,
         scientific >=0.3.4.12 && <0.4,
         unordered-containers >=0.2.8.0 && <0.3
-    cc-options: -Wall -O3 -march=native -std=c++11 -fomit-frame-pointer
-    c-sources:
-        cbits/sajson_wrapper.cpp
-    default-language: Haskell2010
-    extra-libraries:
-        stdc++
-    include-dirs: cbits
-    hs-source-dirs: src
-    ghc-options: -Wall -O2
 
 executable sajson-bench
-    main-is: Bench.hs
+    main-is:          Bench.hs
+    hs-source-dirs:   bench
+    default-language: Haskell2010
+    ghc-options:      -Wall -threaded -rtsopts
     build-depends:
-        base >=4.9.1.0 && <5,
-        sajson,
+        base >=4.9.1.0 && <4.10,
+        sajson -any,
         criterion >=1.1.4.0 && <1.2,
         aeson >=1.0.2.1 && <1.1,
         bytestring >=0.10.8.1 && <0.11
-    default-language: Haskell2010
-    hs-source-dirs: bench
-    ghc-options: -Wall -threaded -rtsopts
 
 test-suite sajson-test
-    type: exitcode-stdio-1.0
-    main-is: Spec.hs
+    type:             exitcode-stdio-1.0
+    main-is:          Spec.hs
+    hs-source-dirs:   test
+    default-language: Haskell2010
+    ghc-options:      -Wall -threaded -rtsopts
     build-depends:
-        base >=4.9.1.0 && <5,
-        sajson,
+        base >=4.9.1.0 && <4.10,
+        sajson -any,
         QuickCheck >=2.9.2 && <2.10,
         aeson >=1.0.2.1 && <1.1,
         bytestring >=0.10.8.1 && <0.11,
@@ -69,6 +73,3 @@
         text >=1.2.2.1 && <1.3,
         unordered-containers >=0.2.8.0 && <0.3,
         vector >=0.11.0.0 && <0.12
-    default-language: Haskell2010
-    hs-source-dirs: test
-    ghc-options: -Wall -threaded -rtsopts
