Packages
glazer
0.5.5
0.5.17
0.5.16
0.5.15
0.5.14
retired
0.5.13
retired
0.5.12
retired
0.5.11
retired
0.5.10
retired
0.5.9
retired
0.5.8
retired
0.5.7
retired
0.5.6
retired
0.5.5
retired
0.5.4
retired
0.5.3
retired
0.5.2
retired
0.5.1
retired
0.5.0
retired
0.3.2
retired
0.3.0
retired
0.2.6
retired
0.2.5
retired
0.2.4
retired
0.2.3
retired
0.2.2
retired
0.2.1
retired
0.2.0
retired
0.1.7
retired
0.1.6
retired
0.1.5
retired
0.1.4
retired
Erlang NIF JSON encoder/decoder using the glaze C++ library
Retired package: Deprecated - Deprecated
Current section
Files
Jump to
Current section
Files
c_src/glazer_json.hpp
// vim:ts=2:sw=2:et
//-----------------------------------------------------------------------------
// JSON-specific decode/encode/scan implementation.
//
// Decode: hand-rolled recursive-descent parser — zero-copy over raw input,
// produces Erlang terms in a single pass (no intermediate tree).
// Encode: direct Erlang-term to JSON writer with a stack-allocated output
// buffer (no intermediate generic_u64 tree).
//-----------------------------------------------------------------------------
#pragma once
#include <array>
#include <cassert>
#include <charconv>
#include <climits>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <string>
#include <string_view>
#include <erl_nif.h>
#include "fast_float.hpp"
#include "glazer_atoms.hpp"
#include "glazer_bigint.hpp"
#include "glazer_common.hpp"
namespace glz {
//-----------------------------------------------------------------------------
// Options
//-----------------------------------------------------------------------------
struct DecodeOpts {
bool object_as_tuple = false;
ERL_NIF_TERM null_term = 0;
bool label_atom = false;
bool label_existing_atom = false;
bool dedupe_keys = false;
};
struct EncodeOpts {
bool pretty = false;
bool uescape = false;
bool force_utf8 = false;
ERL_NIF_TERM null_term = 0;
};
//-----------------------------------------------------------------------------
// Option parsing
//-----------------------------------------------------------------------------
static bool parse_decode_opts(ErlNifEnv* env, ERL_NIF_TERM list, DecodeOpts& opts)
{
ERL_NIF_TERM head, tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_is_identical(head, AM_OBJECT_AS_TUPLE)) opts.object_as_tuple = true;
else if (enif_is_identical(head, AM_USE_NIL)) opts.null_term = AM_NIL;
else if (enif_is_identical(head, AM_DEDUPE_KEYS)) opts.dedupe_keys = true;
else {
int arity; const ERL_NIF_TERM* tp;
if (enif_get_tuple(env, head, &arity, &tp) && arity == 2) {
if (enif_is_identical(tp[0], AM_NULL_TERM) && enif_is_atom(env, tp[1]))
opts.null_term = tp[1];
else if (enif_is_identical(tp[0], AM_KEYS) || enif_is_identical(tp[0], AM_LABEL_ATOM)) {
if (enif_is_identical(tp[1], AM_LABEL_ATOM)) opts.label_atom = true;
else if (enif_is_identical(tp[1], AM_LABEL_EXISTING_ATOM)) opts.label_existing_atom = true;
else if (enif_is_identical(tp[1], AM_LABEL_BINARY)) { opts.label_atom = false; opts.label_existing_atom = false; }
}
}
}
}
return true;
}
static bool parse_encode_opts(ErlNifEnv* env, ERL_NIF_TERM list, EncodeOpts& opts)
{
ERL_NIF_TERM head, tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_is_identical(head, AM_PRETTY)) opts.pretty = true;
else if (enif_is_identical(head, AM_USE_NIL)) opts.null_term = AM_NIL;
else if (enif_is_identical(head, AM_UESCAPE)) opts.uescape = true;
else if (enif_is_identical(head, AM_FORCE_UTF8)) opts.force_utf8 = true;
else {
int arity; const ERL_NIF_TERM* tp;
if (enif_get_tuple(env, head, &arity, &tp) && arity == 2)
if (enif_is_identical(tp[0], AM_NULL_TERM) && enif_is_atom(env, tp[1]))
opts.null_term = tp[1];
}
}
return true;
}
//-----------------------------------------------------------------------------
// Zero-copy JSON decoder — parses raw bytes, emits Erlang terms directly
//-----------------------------------------------------------------------------
struct Decoder {
ErlNifEnv* m_env;
const DecodeOpts& m_opts;
const char* m_beg; // start of input (for error reporting)
const char* m_p; // current position
const char* m_end;
KeyCache m_key_cache;
bool m_use_key_cache;
unsigned m_depth = 0;
std::string m_err;
// Below this input size, documents rarely repeat enough keys to amortize
// the cache's lookup-scan cost — skip it entirely (helps small payloads
// like RPC messages, where glazer otherwise loses ground to torque).
static constexpr size_t KEY_CACHE_MIN_SIZE = 2048;
// parse_value/parse_array/parse_object recurse on each nesting level, so
// an unbounded depth can overflow the C stack and crash the whole VM.
// This cap is well within the default thread stack size with room to
// spare for the rest of each frame, across compilers (gcc/clang) and
// under AddressSanitizer (whose redzones and shadow-memory checks inflate
// each frame considerably compared to a normal build).
static constexpr unsigned MAX_DEPTH = 256;
Decoder(ErlNifEnv* e, const DecodeOpts& o, const char* data, size_t size)
: m_env(e), m_opts(o), m_beg(data), m_p(data), m_end(data + size),
m_use_key_cache(size >= KEY_CACHE_MIN_SIZE) {}
// Increments the shared depth counter for the lifetime of a parse_array /
// parse_object call, so every return path (including early `return 0`)
// restores it.
struct DepthGuard {
explicit DepthGuard(Decoder* d) : d(d) { ++d->m_depth; }
~DepthGuard() { --d->m_depth; }
bool check() const {
if (d->m_depth > MAX_DEPTH) [[unlikely]] {
d->m_err = "exceeded maximum nesting depth";
return false;
}
++d->m_p;
d->skip_ws();
return true;
}
private:
Decoder* d;
};
// ---- whitespace ----
static inline bool is_ws(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }
void skip_ws() {
// Fast path: minified JSON has structural whitespace only rarely (often
// none at all). Check the first byte before paying for an 8-byte load
// and SWAR bit-twiddling — avoids that cost on the overwhelmingly common
// "no whitespace here" case.
if (m_p >= m_end || !is_ws(*m_p)) return;
while (m_p + 8 <= m_end) {
uint64_t w;
memcpy(&w, m_p, 8);
// Any byte that is not one of ' ' \t \r \n stops the run.
uint64_t non_ws = has_byte(w, ' ') | has_byte(w, '\t') | has_byte(w, '\r') | has_byte(w, '\n');
// non_ws has a set high-bit at each position that *matches* one of the WS chars.
// We want the first byte that does NOT match any — invert per-byte "is whitespace" mask.
// Build the set of matched positions, then find first unmatched byte.
uint64_t matched = non_ws;
// A byte fully matches iff its top bit is set in `matched`. Find first byte where it's clear.
uint64_t cleared = ~matched & 0x8080808080808080ULL;
if (cleared) {
#if defined(__GNUC__) || defined(__clang__)
m_p += __builtin_ctzll(cleared) >> 3;
#else
while (is_ws(*m_p)) ++m_p;
#endif
return;
}
m_p += 8;
}
while (m_p < m_end && is_ws(*m_p)) ++m_p;
}
// SWAR (SIMD-within-a-register) helpers: detect '"' or '\' anywhere within
// an 8-byte word in a few branch-free ops. Classic bit-trick:
// for byte b, ((b ^ pattern) - 0x01..) & ~(b ^ pattern) & 0x80.. is set
// iff b == pattern's corresponding byte.
static inline uint64_t has_byte(uint64_t w, uint8_t needle) {
uint64_t pattern = 0x0101010101010101ULL * needle;
uint64_t x = w ^ pattern;
return (x - 0x0101010101010101ULL) & ~x & 0x8080808080808080ULL;
}
// ---- string reading — returns view into raw input (no unescaping for pure-ASCII keys) ----
// Advance m_p past bytes that are neither '"' nor '\' using the widest
// SIMD tier available (AVX2: 32 B, SSE2: 16 B), then SWAR (8 B).
// On return m_p is at the first potential special byte or in the scalar
// cleanup zone (fewer than one chunk-width from m_end).
void bulk_skip_to_special() noexcept {
#if defined(__AVX2__)
{
const __m256i vq = _mm256_set1_epi8('"');
const __m256i vb = _mm256_set1_epi8('\\');
while (m_p + 32 <= m_end) {
__m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(m_p));
uint32_t mask = (uint32_t)_mm256_movemask_epi8(
_mm256_or_si256(_mm256_cmpeq_epi8(v, vq), _mm256_cmpeq_epi8(v, vb)));
if (mask) { m_p += __builtin_ctz(mask); return; }
m_p += 32;
}
}
#endif
#if defined(__SSE2__)
{
const __m128i vq = _mm_set1_epi8('"');
const __m128i vb = _mm_set1_epi8('\\');
while (m_p + 16 <= m_end) {
__m128i v = _mm_loadu_si128(reinterpret_cast<const __m128i*>(m_p));
unsigned mask = (unsigned)_mm_movemask_epi8(
_mm_or_si128(_mm_cmpeq_epi8(v, vq), _mm_cmpeq_epi8(v, vb)));
if (mask) { m_p += __builtin_ctz(mask); return; }
m_p += 16;
}
}
#endif
// SWAR fallback (8 B/iter): leaves m_p at the word boundary before a hit.
while (m_p + 8 <= m_end) {
uint64_t w;
memcpy(&w, m_p, 8);
if (has_byte(w, '"') | has_byte(w, '\\')) break;
m_p += 8;
}
}
// Returns false on error; sets p past the closing quote.
// If has_escape is set the caller must unescape before using as binary.
// Bulk-scans with SIMD (AVX2→SSE2→SWAR); scalar fallback only for the
// few bytes around each '"' or '\'.
bool read_string_raw(const char*& begin_out, size_t& len_out, bool& has_escape)
{
if (m_p >= m_end || *m_p != '"')
return false;
++m_p; // skip opening quote
const char* s = m_p;
has_escape = false;
for (;;) {
bulk_skip_to_special();
while (m_p < m_end) {
char c = *m_p;
if (c == '"') { begin_out = s; len_out = m_p - s; ++m_p; return true; }
if (c == '\\') [[unlikely]] { has_escape = true; ++m_p; if (m_p < m_end) ++m_p; break; }
++m_p;
}
if (m_p >= m_end) return false; // unterminated string
}
}
// Unescape a JSON string into buf, return view of result.
// Only called when has_escape is true. std::string is used deliberately:
// escaped strings are rare in practice (real payloads have ~0), so the
// buffer is almost never written. If used OutBuf, it would reserve 4 KB on
// the stack unconditionally; std::string pays nothing until the first actual
// write.
static std::string_view unescape(const char* s, size_t len, std::string& buf)
{
buf.clear();
buf.reserve(len);
const char* end = s + len;
while (s < end) {
char c = *s++;
if (c != '\\') { buf += c; continue; }
if (s >= end) break;
switch (*s++) {
case '"': buf += '"'; break;
case '\\': buf += '\\'; break;
case '/': buf += '/'; break;
case 'b': buf += '\b'; break;
case 'f': buf += '\f'; break;
case 'n': buf += '\n'; break;
case 'r': buf += '\r'; break;
case 't': buf += '\t'; break;
case 'u': {
if (s + 4 > end) break;
auto hex4 = [](const char* p) {
int v = 0;
for (int i = 0; i < 4; ++i) {
char c = p[i];
int d = (c >= '0' && c <= '9') ? c - '0'
: (c >= 'a' && c <= 'f') ? c - 'a' + 10
: (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1;
if (d < 0) return -1;
v = v * 16 + d;
}
return v;
};
int cp = hex4(s); s += 4;
if (cp >= 0xD800 && cp <= 0xDBFF && s + 6 <= end && s[0] == '\\' && s[1] == 'u') {
int lo = hex4(s + 2); s += 6;
if (lo >= 0xDC00 && lo <= 0xDFFF)
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
}
// Encode cp as UTF-8
if (cp < 0x80) { buf += (char)cp; }
else if (cp < 0x800) { buf += (char)(0xC0|(cp>>6)); buf += (char)(0x80|(cp&0x3F)); }
else if (cp < 0x10000) {
buf += (char)(0xE0|(cp>>12)); buf += (char)(0x80|((cp>>6)&0x3F)); buf += (char)(0x80|(cp&0x3F));
} else {
buf += (char)(0xF0|(cp>>18)); buf += (char)(0x80|((cp>>12)&0x3F));
buf += (char)(0x80|((cp>>6)&0x3F)); buf += (char)(0x80|(cp&0x3F));
}
break;
}
default: buf += *(s-1); break;
}
}
return buf;
}
// Make an Erlang binary from a JSON string span (handles escapes).
// buf is scratch storage reused across calls.
ERL_NIF_TERM make_string_term(const char* s, size_t len, bool has_escape, std::string& buf)
{
std::string_view sv = has_escape ? unescape(s, len, buf) : std::string_view(s, len);
return make_binary(m_env, sv);
}
// Make a key term (binary / atom / existing_atom).
ERL_NIF_TERM make_key_term(const char* s, size_t len, bool has_escape, std::string& buf)
{
if (m_opts.label_atom) {
std::string_view sv = has_escape ? unescape(s, len, buf) : std::string_view(s, len);
return enif_make_atom_len(m_env, sv.data(), sv.size());
}
if (m_opts.label_existing_atom) {
std::string_view sv = has_escape ? unescape(s, len, buf) : std::string_view(s, len);
ERL_NIF_TERM t;
// enif_make_existing_atom_len avoids the std::string copy the old code paid
return enif_make_existing_atom_len(m_env, sv.data(), sv.size(), &t, ERL_NIF_LATIN1)
? t : make_binary(m_env, sv);
}
// Binary keys: reuse cached terms for repeated keys (raw, unescaped only —
// escapes are rare for keys and not worth complicating the cache for).
// Only worthwhile for larger documents — see KEY_CACHE_MIN_SIZE.
if (!has_escape && m_use_key_cache) {
uint32_t h = KeyCache::hash_of(s, len);
if (ERL_NIF_TERM cached = m_key_cache.lookup(s, len, h))
return cached;
auto term = make_binary(m_env, std::string_view(s, len));
m_key_cache.insert(s, len, h, term);
return term;
}
return make_string_term(s, len, has_escape, buf);
}
// ---- number parsing ----
ERL_NIF_TERM parse_number()
{
const char* start = m_p;
bool neg = (*m_p == '-');
if (neg) ++m_p;
// Integer part
while (m_p < m_end && *m_p >= '0' && *m_p <= '9') ++m_p;
bool is_float = false;
if (m_p < m_end && *m_p == '.') { is_float = true; ++m_p; while (m_p < m_end && *m_p >= '0' && *m_p <= '9') ++m_p; }
if (m_p < m_end && (*m_p == 'e' || *m_p == 'E')) {
is_float = true;
if (++m_p < m_end && (*m_p == '+' || *m_p == '-')) ++m_p;
while (m_p < m_end && *m_p >= '0' && *m_p <= '9') ++m_p;
}
if (is_float) {
double d;
// std::from_chars for floating-point isn't available on all platforms
// (e.g. older Apple libc++), so use vendored fast_float here.
auto [ep, ec] = glz::fast_float::from_chars(start, m_p, d);
if (ec != std::errc{}) return 0;
return enif_make_double(m_env, d);
}
// Integer: try int64/uint64 first, bigint fallback
if (neg) {
int64_t v = 0;
auto [ep, ec] = std::from_chars(start + 1, m_p, v);
if (ec == std::errc{})
return enif_make_int64(m_env, -v);
// Could be uint64_t range negative? no — fall through to bigint
} else {
uint64_t v = 0;
auto [ep, ec] = std::from_chars(start, m_p, v);
if (ec == std::errc{})
return v <= uint64_t(INT64_MAX) ? enif_make_int64(m_env, int64_t(v))
: enif_make_uint64(m_env, v);
}
// Bigint
ERL_NIF_TERM r = glz::BigInt::decode(m_env, start, m_p);
return r ? r : (ERL_NIF_TERM)0;
}
// ---- core value parser ----
ERL_NIF_TERM parse_value(std::string& scratch)
{
skip_ws();
if (m_p >= m_end) [[unlikely]]
return 0;
switch (*m_p) {
case '"': {
const char* s; size_t len; bool has_escape;
if (!read_string_raw(s, len, has_escape)) return 0;
return make_string_term(s, len, has_escape, scratch);
}
case '{': return parse_object(scratch);
case '[': return parse_array(scratch);
case 't':
if (m_p + 4 <= m_end && memcmp(m_p, "true", 4) == 0) { m_p += 4; return AM_TRUE; } return 0;
case 'f':
if (m_p + 5 <= m_end && memcmp(m_p, "false", 5) == 0) { m_p += 5; return AM_FALSE; } return 0;
case 'n':
if (m_p + 4 <= m_end && memcmp(m_p, "null", 4) == 0) { m_p += 4; return m_opts.null_term; } return 0;
case '-': case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
return parse_number();
default: return 0;
}
}
ERL_NIF_TERM parse_array(std::string& scratch)
{
assert(*m_p == '[');
DepthGuard guard(this);
if (!guard.check()) [[unlikely]] return 0;
SmallTermVec<16> items;
if (m_p < m_end && *m_p == ']') {
++m_p;
return enif_make_list_from_array(m_env, nullptr, 0);
}
for (;;) {
auto v = parse_value(scratch);
if (!v) [[unlikely]] return 0;
items.push_back(v);
skip_ws();
if (m_p >= m_end) [[unlikely]] return 0;
if (*m_p == ']') { ++m_p; break; }
if (*m_p != ',') [[unlikely]] return 0;
++m_p;
}
return enif_make_list_from_array(m_env, items.data(), unsigned(items.size()));
}
// Remove earlier duplicate {Key, Val} pairs in-place, keeping each key's
// *last* occurrence (and its position). O(n^2) but objects are typically
// small (SmallTermVec inline capacity is 32) so this is cheaper than a
// hash set for the common case. Used for the object_as_tuple path, which
// has no map-based shortcut.
template <size_t N>
static void dedupe_pairs_last(SmallTermVec<N>& pairs, ErlNifEnv* env)
{
size_t n = pairs.size();
size_t out = 0;
for (size_t i = 0; i < n; ++i) {
int arity_i; const ERL_NIF_TERM* tp_i;
enif_get_tuple(env, pairs.data()[i], &arity_i, &tp_i);
bool dup = false;
for (size_t j = i + 1; j < n; ++j) {
int arity_j; const ERL_NIF_TERM* tp_j;
enif_get_tuple(env, pairs.data()[j], &arity_j, &tp_j);
if (enif_compare(tp_i[0], tp_j[0]) == 0) { dup = true; break; }
}
if (!dup)
pairs.data()[out++] = pairs.data()[i];
}
pairs.set_size(out);
}
ERL_NIF_TERM parse_object(std::string& scratch)
{
assert(*m_p == '{');
DepthGuard guard(this);
if (!guard.check()) [[unlikely]] return 0;
if (m_opts.object_as_tuple) {
SmallTermVec<32> pairs;
if (m_p < m_end && *m_p == '}') { ++m_p;
return enif_make_tuple1(m_env, enif_make_list_from_array(m_env, nullptr, 0)); }
for (;;) {
if (m_p >= m_end || *m_p != '"') [[unlikely]] return 0;
const char* ks;
size_t kl;
bool ke;
if (!read_string_raw(ks, kl, ke)) [[unlikely]] return 0;
auto key = make_key_term(ks, kl, ke, scratch);
skip_ws();
if (m_p >= m_end || *m_p != ':') [[unlikely]] return 0;
++m_p;
auto val = parse_value(scratch);
if (!val) [[unlikely]] return 0;
pairs.push_back(enif_make_tuple2(m_env, key, val));
skip_ws();
if (m_p >= m_end) [[unlikely]] return 0;
if (*m_p == '}') { ++m_p; break; }
if (*m_p != ',') [[unlikely]] return 0;
++m_p;
skip_ws();
}
if (m_opts.dedupe_keys)
dedupe_pairs_last(pairs, m_env);
return enif_make_tuple1(m_env, pairs.to_erl_list(m_env));
}
// Map path
SmallTermVec<32> ks, vs;
if (m_p < m_end && *m_p == '}') {
++m_p;
return enif_make_new_map(m_env);
}
for (;;) {
if (m_p >= m_end || *m_p != '"') [[unlikely]] return 0;
const char* kstr;
size_t klen;
bool kesc;
if (!read_string_raw(kstr, klen, kesc))
[[unlikely]] return 0;
auto key = make_key_term(kstr, klen, kesc, scratch);
skip_ws();
if (m_p >= m_end || *m_p != ':') [[unlikely]] return 0;
++m_p;
auto val = parse_value(scratch);
if (!val) [[unlikely]] return 0;
ks.push_back(key); vs.push_back(val);
skip_ws();
if (m_p >= m_end) [[unlikely]] return 0;
if (*m_p == '}') { ++m_p; break; }
if (*m_p != ',') [[unlikely]] return 0;
++m_p;
skip_ws();
}
[[maybe_unused]] auto map = vs.to_erl_map<true>(m_env, ks);
assert(map);
return map;
}
// Always returns {ok, Term} | {error, Msg}.
// Raising vs. non-raising behaviour is the Erlang caller's responsibility.
std::tuple<bool, ERL_NIF_TERM> decode(const char* data, size_t size)
{
m_p = data; m_end = data + size; m_beg = data;
std::string scratch;
ERL_NIF_TERM result = parse_value(scratch);
if (result) skip_ws();
if (result && m_p == m_end) [[likely]]
return std::make_tuple(true, result);
std::string msg = m_err.empty()
? "JSON parse error at offset " + std::to_string(m_p - m_beg)
: m_err + " at offset " + std::to_string(m_p - m_beg);
return std::make_tuple(false, make_binary(m_env, msg));
}
};
//-----------------------------------------------------------------------------
// Value-boundary scanner — finds where the next complete top-level JSON value
// ends in a (possibly partial) buffer, without building any Erlang terms.
//
// This underpins incremental/streaming decode: callers buffer raw bytes,
// repeatedly ask the scanner "is there a complete value yet?", and once one
// is found, slice it off and hand it to the existing whole-buffer `decode`.
// The scanner never allocates and never inspects string contents beyond
// quote/escape bytes, so it stays cheap even on huge inputs.
//-----------------------------------------------------------------------------
struct ScanState {
uint64_t pos = 0; // byte offset into the buffer to resume scanning at
uint32_t depth = 0; // current [ ]/{ } nesting depth
bool in_string = false; // currently inside a "..." (top-level or nested)
bool escape = false; // previous byte inside a string was an unconsumed backslash
bool started = false; // have we seen the first non-ws byte of the value yet?
bool scalar = false; // value-so-far is a bare scalar (number/literal), not { or [
static ScanState initial() { return ScanState{}; }
};
struct Scanner {
const char* m_beg;
const char* m_p;
const char* m_end;
// `resume_pos` is where to start scanning from (0 for a fresh scan, or
// ScanState::pos when continuing — the caller passes the full buffer each
// time, so previously-scanned bytes must be skipped rather than re-walked).
Scanner(const char* data, size_t size, size_t resume_pos)
: m_beg(data), m_p(data + std::min(resume_pos, size)), m_end(data + size) {}
static inline bool is_ws(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }
// Scans from `p` using/updating `st`.
// returns true + sets `value_end` to one-past-the-last-byte of the value, if complete
// returns false (value_end untouched) if the buffer ran out mid-value (st updated to resume)
bool scan(ScanState& st, const char*& value_end)
{
// Skip leading whitespace before the value starts.
if (!st.started) {
while (m_p < m_end && is_ws(*m_p)) ++m_p;
if (m_p >= m_end) return false;
}
while (m_p < m_end) {
char c = *m_p;
if (st.in_string) {
if (st.escape) { st.escape = false; ++m_p; continue; }
if (c == '\\') { st.escape = true; ++m_p; continue; }
if (c == '"') { st.in_string = false; ++m_p;
if (st.depth == 0 && st.scalar) { value_end = m_p; return true; }
continue;
}
++m_p;
continue;
}
switch (c) {
case '"':
st.in_string = true;
if (!st.started) { st.started = true; st.scalar = true; }
++m_p;
break;
case '{':
case '[':
st.started = true;
st.scalar = false;
++st.depth;
++m_p;
break;
case '}':
case ']':
if (st.depth == 0) { value_end = m_p; return true; } // stray close — treat as boundary
--st.depth;
++m_p;
if (st.depth == 0) { value_end = m_p; return true; }
break;
default:
if (st.depth == 0) {
if (is_ws(c)) {
if (st.started && st.scalar) { value_end = m_p; return true; }
++m_p;
} else if (!st.started) {
// start of a bare scalar: number, true/false/null
st.started = true;
st.scalar = true;
++m_p;
} else if (st.scalar) {
++m_p;
} else {
// garbage after a completed container value
value_end = m_p;
return true;
}
} else {
++m_p; // inside a container: commas, colons, scalar bytes — just consume
}
break;
}
}
// Ran out of input — record where to resume from on the next call. Note
// that even a "complete-looking" bare scalar at the buffer boundary is
// ambiguous (more digits/letters could follow in the next chunk), so we
// always report incomplete here and let the caller feed more data or
// signal EOF explicitly.
st.pos = static_cast<uint64_t>(m_p - m_beg);
return false;
}
};
inline ERL_NIF_TERM scan_state_to_term(ErlNifEnv* env, const ScanState& st)
{
return enif_make_tuple6(env,
enif_make_uint64(env, st.pos),
enif_make_uint(env, st.depth),
st.in_string ? AM_TRUE : AM_FALSE,
st.escape ? AM_TRUE : AM_FALSE,
st.started ? AM_TRUE : AM_FALSE,
st.scalar ? AM_TRUE : AM_FALSE);
}
inline bool scan_state_from_term(ErlNifEnv* env, ERL_NIF_TERM term, ScanState& st)
{
int arity; const ERL_NIF_TERM* tp;
if (!enif_get_tuple(env, term, &arity, &tp) || arity != 6) return false;
ErlNifUInt64 pos;
unsigned depth;
if (!enif_get_uint64(env, tp[0], &pos)) return false;
if (!enif_get_uint(env, tp[1], &depth)) return false;
st.pos = pos;
st.depth = depth;
st.in_string = enif_is_identical(tp[2], AM_TRUE);
st.escape = enif_is_identical(tp[3], AM_TRUE);
st.started = enif_is_identical(tp[4], AM_TRUE);
st.scalar = enif_is_identical(tp[5], AM_TRUE);
return true;
}
//-----------------------------------------------------------------------------
// Direct Erlang-term → JSON encoder (no intermediate generic_u64 tree)
//-----------------------------------------------------------------------------
// Bytes that must be escaped in a JSON string: control chars, '"', '\'.
// Everything else (including all UTF-8 continuation/lead bytes) passes through.
static constexpr bool needs_escape(unsigned char c) {
return c < 0x20 || c == '"' || c == '\\';
}
static constexpr std::array<bool, 256> build_needs_escape_tab() {
std::array<bool, 256> tab{};
for (int i = 0; i < 256; ++i) tab[i] = needs_escape((unsigned char)i);
return tab;
}
static constexpr std::array<bool, 256> NEEDS_ESCAPE_TAB = build_needs_escape_tab();
// Return a pointer to the first byte in [p, end) that needs JSON escaping
// (control char < 0x20, '"', or '\'). Returns end if none found.
// Uses AVX2 (32 B/iter) → SSE2 (16 B/iter) → byte table, whichever the
// CPU supports. The bias trick converts unsigned c < 0x20 to a signed
// comparison: (c ^ 0x80) < 0xA0 (signed), which SSE2 _cmpgt handles.
static const char* find_escape_pos(const char* p, const char* end) noexcept
{
#if defined(__AVX2__)
{
const __m256i vq = _mm256_set1_epi8('"');
const __m256i vbs = _mm256_set1_epi8('\\');
const __m256i vbias = _mm256_set1_epi8(-128); // XOR: unsigned→signed shift
const __m256i vcmp = _mm256_set1_epi8(-96); // 0xA0: biased < this ↔ c < 0x20
while (p + 32 <= end) {
__m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(p));
__m256i biased = _mm256_xor_si256(v, vbias);
uint32_t mask = (uint32_t)_mm256_movemask_epi8(_mm256_or_si256(_mm256_or_si256(
_mm256_cmpgt_epi8(vcmp, biased),
_mm256_cmpeq_epi8(v, vq)),
_mm256_cmpeq_epi8(v, vbs)));
if (mask) return p + __builtin_ctz(mask);
p += 32;
}
}
#endif
#if defined(__SSE2__)
{
const __m128i vq = _mm_set1_epi8('"');
const __m128i vbs = _mm_set1_epi8('\\');
const __m128i vbias = _mm_set1_epi8(-128);
const __m128i vcmp = _mm_set1_epi8(-96);
while (p + 16 <= end) {
__m128i v = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
__m128i biased = _mm_xor_si128(v, vbias);
unsigned mask = (unsigned)_mm_movemask_epi8(_mm_or_si128(_mm_or_si128(
_mm_cmpgt_epi8(vcmp, biased),
_mm_cmpeq_epi8(v, vq)),
_mm_cmpeq_epi8(v, vbs)));
if (mask) return p + __builtin_ctz(mask);
p += 16;
}
}
#endif
while (p < end && !NEEDS_ESCAPE_TAB[(unsigned char)*p]) ++p;
return p;
}
// JSON-escape a UTF-8 byte sequence into out.
// Uses find_escape_pos to bulk-skip clean runs (AVX2/SSE2/table); only the
// per-byte switch runs for the rare escape characters.
static void json_escape_string(std::string_view sv, OutBuf& out)
{
out.push('"');
const char* p = sv.data();
const char* end = p + sv.size();
while (p < end) {
const char* special = find_escape_pos(p, end);
if (special > p) out.push(p, special - p);
p = special;
if (p >= end) break;
unsigned char c = (unsigned char)*p++;
switch (c) {
case '"': out.push("\\\"", 2); break;
case '\\': out.push("\\\\", 2); break;
case '\b': out.push("\\b", 2); break;
case '\f': out.push("\\f", 2); break;
case '\n': out.push("\\n", 2); break;
case '\r': out.push("\\r", 2); break;
case '\t': out.push("\\t", 2); break;
default: {
char esc[6]; write_uescape(esc, c);
out.push(esc, 6);
break;
}
}
}
out.push('"');
}
// JSON-escape a UTF-8 byte sequence, additionally escaping every non-ASCII
// code point as \uXXXX (uescape), and/or replacing invalid UTF-8 byte
// sequences with U+FFFD before escaping (force_utf8).
static void json_escape_string_unicode(std::string_view sv, OutBuf& out,
bool uescape, bool force_utf8)
{
out.push('"');
const char* p = sv.data();
const char* end = p + sv.size();
const char* run = p;
while (p < end) {
auto c = (unsigned char)*p;
if (c < 0x80) [[likely]] {
if (!NEEDS_ESCAPE_TAB[c]) [[likely]] {
++p;
} else {
if (p > run) out.push(run, p - run);
switch (c) {
case '"': out.push("\\\"", 2); break;
case '\\': out.push("\\\\", 2); break;
case '\b': out.push("\\b", 2); break;
case '\f': out.push("\\f", 2); break;
case '\n': out.push("\\n", 2); break;
case '\r': out.push("\\r", 2); break;
case '\t': out.push("\\t", 2); break;
default: push_uescape(out,c); break;
}
++p;
run = p;
}
continue;
}
// Non-ASCII: decode the code point (sanitizing invalid sequences when
// force_utf8 is set; otherwise pass invalid bytes through verbatim).
if (p > run) out.push(run, p - run);
const char* seq_start = p;
uint32_t cp = decode_utf8(p, end);
if (uescape) {
push_uescape(out, cp);
} else if (force_utf8 && cp == 0xFFFD && !(p - seq_start == 3 &&
uint8_t(seq_start[0]) == 0xEF &&
uint8_t(seq_start[1]) == 0xBF &&
uint8_t(seq_start[2]) == 0xBD)) {
// Invalid sequence sanitized to U+FFFD (and it wasn't already a
// literal U+FFFD in the input) — emit the replacement character.
out.push("\xEF\xBF\xBD", 3);
} else {
out.push(seq_start, p - seq_start);
}
run = p;
}
if (p > run) out.push(run, p - run);
out.push('"');
}
struct Encoder {
ErlNifEnv* m_env;
const EncodeOpts& m_opts;
OutBuf& m_out;
char m_atom_buf[256]; // scratch for atom → string_view
void escape_string(std::string_view sv)
{
if (m_opts.uescape || m_opts.force_utf8)
json_escape_string_unicode(sv, m_out, m_opts.uescape, m_opts.force_utf8);
else
json_escape_string(sv, m_out);
}
bool encode(ERL_NIF_TERM term)
{
// Dispatch on the term's runtime type once — avoids the cascade of
// enif_is_identical / enif_get_* probes that each cost a C call.
switch (enif_term_type(m_env, term)) {
case ERL_NIF_TERM_TYPE_BITSTRING: {
ErlNifBinary bin;
if (!enif_inspect_binary(m_env, term, &bin)) return false;
escape_string({reinterpret_cast<const char*>(bin.data), bin.size});
return true;
}
case ERL_NIF_TERM_TYPE_INTEGER:
return glz::BigInt::encode(m_env, term, m_out);
case ERL_NIF_TERM_TYPE_MAP: {
m_out.push('{');
ErlNifMapIterator iter;
if (!enif_map_iterator_create(m_env, term, &iter, ERL_NIF_MAP_ITERATOR_FIRST))
return false;
ERL_NIF_TERM k, v;
bool first = true;
while (enif_map_iterator_get_pair(m_env, &iter, &k, &v)) {
if (!first) m_out.push(',');
first = false;
if (!encode_key(k)) { enif_map_iterator_destroy(m_env, &iter); return false; }
m_out.push(':');
if (!encode(v)) { enif_map_iterator_destroy(m_env, &iter); return false; }
enif_map_iterator_next(m_env, &iter);
}
enif_map_iterator_destroy(m_env, &iter);
m_out.push('}');
return true;
}
case ERL_NIF_TERM_TYPE_LIST: {
m_out.push('[');
ERL_NIF_TERM h, t = term;
bool first = true;
while (enif_get_list_cell(m_env, t, &h, &t)) {
if (!first) m_out.push(',');
first = false;
if (!encode(h)) return false;
}
m_out.push(']');
return true;
}
case ERL_NIF_TERM_TYPE_ATOM: {
if (enif_is_identical(term, m_opts.null_term)) { m_out.push("null", 4); return true; }
if (enif_is_identical(term, AM_TRUE)) { m_out.push("true", 4); return true; }
if (enif_is_identical(term, AM_FALSE)) { m_out.push("false", 5); return true; }
if (enif_is_identical(term, AM_NULL)) { m_out.push("null", 4); return true; }
if (enif_is_identical(term, AM_NIL)) { m_out.push("null", 4); return true; }
std::string_view sv;
if (!atom_to_sv(m_env, term, m_atom_buf, sizeof(m_atom_buf), sv)) return false;
escape_string(sv);
return true;
}
case ERL_NIF_TERM_TYPE_FLOAT: {
double d;
if (!enif_get_double(m_env, term, &d)) return false;
if (!std::isfinite(d)) { m_out.push("null", 4); return true; }
char buf[32];
auto [e, ec] = std::to_chars(buf, buf+32, d, std::chars_format::general);
if (ec == std::errc{}) {
bool has_dot = false;
for (char* p = buf; p < e; ++p) if (*p == '.' || *p == 'e' || *p == 'E') { has_dot = true; break; }
m_out.push(buf, e - buf);
if (!has_dot) m_out.push(".0", 2);
} else {
int n = snprintf(buf, sizeof(buf), "%.17g", d);
m_out.push(buf, n);
}
return true;
}
case ERL_NIF_TERM_TYPE_TUPLE: {
// {[{K,V}...]} proplist → object
int arity; const ERL_NIF_TERM* tp;
enif_get_tuple(m_env, term, &arity, &tp);
if (arity == 1 && enif_is_list(m_env, tp[0])) {
m_out.push('{');
ERL_NIF_TERM h, t = tp[0];
bool first = true;
while (enif_get_list_cell(m_env, t, &h, &t)) {
int pa; const ERL_NIF_TERM* pp;
if (!enif_get_tuple(m_env, h, &pa, &pp) || pa != 2) return false;
if (!first) m_out.push(',');
first = false;
if (!encode_key(pp[0])) return false;
m_out.push(':');
if (!encode(pp[1])) return false;
}
m_out.push('}');
return true;
}
return false;
}
default:
return false;
}
}
bool encode_key(ERL_NIF_TERM k)
{
ErlNifBinary bin;
if (enif_inspect_binary(m_env, k, &bin)) {
escape_string({reinterpret_cast<const char*>(bin.data), bin.size});
return true;
}
if (enif_is_atom(m_env, k)) {
std::string_view sv;
if (!atom_to_sv(m_env, k, m_atom_buf, sizeof(m_atom_buf), sv)) return false;
escape_string(sv);
return true;
}
return false;
}
};
} // namespace glz