// vim:ts=2:sw=2:et
//-----------------------------------------------------------------------------
// YAML-specific decode implementation.
//
// Decode: hand-rolled recursive-descent block-style parser — produces
// Erlang terms directly in a single pass, mirroring the philosophy
// of the JSON decoder in glazer_json.hpp (no intermediate tree).
//
// Not yet implemented: tags (!!str etc.), multi-document streams, complex
// (collection) mapping keys, merge-key (<<) semantics, anchors on mapping
// keys, top-level (root-node) anchors/aliases.
//-----------------------------------------------------------------------------
#pragma once
#include <cassert>
#include <cctype>
#include <charconv>
#include <climits>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <map>
#include <string>
#include <string_view>
#include <vector>
#include <erl_nif.h>
#if defined(__ARM_NEON__)
# include <arm_neon.h>
#endif
#include "fast_float.hpp"
#include "glazer_atoms.hpp"
#include "glazer_bigint.hpp"
#include "glazer_common.hpp"
namespace glz {
//-----------------------------------------------------------------------------
// SIMD helpers for the YAML decoder/encoder.
//
// find_break: first '\n' or '\r' (end-of-line scan)
// find_dq_special: first '"', '\', '\n', '\r' (double-quoted scalar scan)
// find_byte (glazer_common.hpp): first '\'' (single-quoted scalar encoder)
//
// AVX2 (32 B/iter) → SSE2 (16 B/iter) → scalar fallback.
//-----------------------------------------------------------------------------
static const char* find_break(const char* p, const char* end) noexcept
{
#if defined(__ARM_NEON__)
{
const uint8x16_t vn = vdupq_n_u8('\n');
const uint8x16_t vr = vdupq_n_u8('\r');
while (p + 16 <= end) {
uint8x16_t v = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
uint8x16_t hit = vorrq_u8(vceqq_u8(v, vn), vceqq_u8(v, vr));
uint64x2_t h64 = vreinterpretq_u64_u8(hit);
uint64_t lo = vgetq_lane_u64(h64, 0);
uint64_t hi = vgetq_lane_u64(h64, 1);
if (lo | hi) {
if (lo) return p + (__builtin_ctzll(lo) >> 3);
return p + 8 + (__builtin_ctzll(hi) >> 3);
}
p += 16;
}
}
#endif
#if defined(__AVX2__)
{
const __m256i vn = _mm256_set1_epi8('\n');
const __m256i vr = _mm256_set1_epi8('\r');
while (p + 32 <= end) {
__m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(p));
uint32_t mask = (uint32_t)_mm256_movemask_epi8(
_mm256_or_si256(_mm256_cmpeq_epi8(v, vn), _mm256_cmpeq_epi8(v, vr)));
if (mask) return p + __builtin_ctz(mask);
p += 32;
}
}
#endif
#if defined(__SSE2__)
{
const __m128i vn = _mm_set1_epi8('\n');
const __m128i vr = _mm_set1_epi8('\r');
while (p + 16 <= end) {
__m128i v = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
unsigned mask = (unsigned)_mm_movemask_epi8(
_mm_or_si128(_mm_cmpeq_epi8(v, vn), _mm_cmpeq_epi8(v, vr)));
if (mask) return p + __builtin_ctz(mask);
p += 16;
}
}
#endif
while (p < end && *p != '\n' && *p != '\r') ++p;
return p;
}
// Returns a pointer to the first byte in [p, end) that is '"', '\', '\n',
// or '\r' — the four characters that require special handling inside a
// double-quoted YAML scalar.
static const char* find_dq_special(const char* p, const char* end) noexcept
{
#if defined(__ARM_NEON__)
{
const uint8x16_t vq = vdupq_n_u8('"');
const uint8x16_t vbs = vdupq_n_u8('\\');
const uint8x16_t vn = vdupq_n_u8('\n');
const uint8x16_t vr = vdupq_n_u8('\r');
while (p + 16 <= end) {
uint8x16_t v = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
uint8x16_t hit = vorrq_u8(vorrq_u8(vceqq_u8(v, vq), vceqq_u8(v, vbs)),
vorrq_u8(vceqq_u8(v, vn), vceqq_u8(v, vr)));
uint64x2_t h64 = vreinterpretq_u64_u8(hit);
uint64_t lo = vgetq_lane_u64(h64, 0);
uint64_t hi = vgetq_lane_u64(h64, 1);
if (lo | hi) {
if (lo) return p + (__builtin_ctzll(lo) >> 3);
return p + 8 + (__builtin_ctzll(hi) >> 3);
}
p += 16;
}
}
#endif
#if defined(__AVX2__)
{
const __m256i vq = _mm256_set1_epi8('"');
const __m256i vbs = _mm256_set1_epi8('\\');
const __m256i vn = _mm256_set1_epi8('\n');
const __m256i vr = _mm256_set1_epi8('\r');
while (p + 32 <= end) {
__m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(p));
uint32_t mask = (uint32_t)_mm256_movemask_epi8(_mm256_or_si256(
_mm256_or_si256(_mm256_cmpeq_epi8(v, vq), _mm256_cmpeq_epi8(v, vbs)),
_mm256_or_si256(_mm256_cmpeq_epi8(v, vn), _mm256_cmpeq_epi8(v, vr))));
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 vn = _mm_set1_epi8('\n');
const __m128i vr = _mm_set1_epi8('\r');
while (p + 16 <= end) {
__m128i v = _mm_loadu_si128(reinterpret_cast<const __m128i*>(p));
unsigned mask = (unsigned)_mm_movemask_epi8(_mm_or_si128(
_mm_or_si128(_mm_cmpeq_epi8(v, vq), _mm_cmpeq_epi8(v, vbs)),
_mm_or_si128(_mm_cmpeq_epi8(v, vn), _mm_cmpeq_epi8(v, vr))));
if (mask) return p + __builtin_ctz(mask);
p += 16;
}
}
#endif
while (p < end && *p != '"' && *p != '\\' && *p != '\n' && *p != '\r') ++p;
return p;
}
//-----------------------------------------------------------------------------
// Options
//-----------------------------------------------------------------------------
struct YAMLDecodeOpts {
ERL_NIF_TERM null_term = 0;
bool hdr_atom = false;
bool hdr_existing_atom = false;
bool yaml_1_1_bools = false;
// When true, scalars are copied into fresh binaries instead of referencing
// the input via sub-binary. Use this when decoded scalars will outlive the
// input binary by a large margin: without it, one live scalar keeps the
// entire input buffer from being collected.
bool copy_strings = false;
};
static bool parse_yaml_decode_opts(ErlNifEnv* env, ERL_NIF_TERM list, YAMLDecodeOpts& opts)
{
ERL_NIF_TERM head, tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_is_identical(head, AM_USE_NIL)) opts.null_term = AM_NIL;
else if (enif_is_identical(head, AM_YAML_1_1_BOOLS)) opts.yaml_1_1_bools = true;
else if (enif_is_identical(head, AM_COPY_STRINGS)) opts.copy_strings = 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)) {
if (enif_is_identical(tp[1], AM_ATOM)) opts.hdr_atom = true;
else if (enif_is_identical(tp[1], AM_EXISTING_ATOM)) opts.hdr_existing_atom = true;
else if (enif_is_identical(tp[1], AM_LABEL_BINARY)) { opts.hdr_atom = false; opts.hdr_existing_atom = false; }
}
}
}
}
return true;
}
struct YAMLEncodeOpts {
ERL_NIF_TERM null_term = 0;
};
static bool parse_yaml_encode_opts(ErlNifEnv* env, ERL_NIF_TERM list, YAMLEncodeOpts& opts)
{
ERL_NIF_TERM head, tail = list;
while (enif_get_list_cell(env, tail, &head, &tail)) {
if (enif_is_identical(head, AM_USE_NIL)) opts.null_term = AM_NIL;
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;
}
//-----------------------------------------------------------------------------
// YAML block-style decoder
//-----------------------------------------------------------------------------
struct YAMLDecoder {
ErlNifEnv* m_env;
const YAMLDecodeOpts& m_opts;
const char* m_beg;
const char* m_p;
const char* m_end;
ERL_NIF_TERM m_input_bin; // original binary term — used for zero-copy sub_binary
KeyCache m_key_cache;
bool m_use_key_cache;
unsigned m_depth = 0;
std::string m_err;
// &anchor bindings — terms live in m_env, which outlives the decoder.
std::map<std::string, ERL_NIF_TERM, std::less<>> m_anchors;
static constexpr size_t KEY_CACHE_MIN_SIZE = 2048;
static constexpr unsigned MAX_DEPTH = 256;
YAMLDecoder(ErlNifEnv* e, const YAMLDecodeOpts& o, const char* data, size_t size,
ERL_NIF_TERM input_bin)
: m_env(e), m_opts(o), m_beg(data), m_p(data), m_end(data + size),
m_input_bin(input_bin), m_use_key_cache(size >= KEY_CACHE_MIN_SIZE) {}
struct DepthGuard {
explicit DepthGuard(YAMLDecoder* d) : d(d) { ++d->m_depth; }
~DepthGuard() { --d->m_depth; }
bool ok() const { return d->m_depth <= MAX_DEPTH; }
private:
YAMLDecoder* d;
};
// -------------------------------------------------------------------------
// Low-level helpers
// -------------------------------------------------------------------------
static inline bool is_blank(char c) { return c == ' ' || c == '\t'; }
static inline bool is_break(char c) { return c == '\n' || c == '\r'; }
static inline bool is_flow_indicator(char c) {
return c == ',' || c == '[' || c == ']' || c == '{' || c == '}';
}
bool at_end() const { return m_p >= m_end; }
// Skip a single line break (\r\n, \r, or \n).
void skip_break() {
if (m_p < m_end && *m_p == '\r') ++m_p;
if (m_p < m_end && *m_p == '\n') ++m_p;
}
// Skip blanks (space/tab) only.
void skip_blanks() { while (m_p < m_end && is_blank(*m_p)) ++m_p; }
// Skip a trailing comment (# ... ) to end of line. Caller has already
// verified we're not inside a quoted scalar. A '#' only starts a comment
// if it's at the start of the line or preceded by whitespace.
void skip_comment_to_eol() {
m_p = find_break(m_p, m_end);
}
// Skip blank lines and comment-only lines, and any line whose content
// (after indentation) is empty or starts with '#'. Also tolerates
// `---`/`...` document marker lines (single-document mode: no-ops).
// Leaves m_p at the start of the next line that has real content, or at
// m_end.
void skip_blank_and_comment_lines() {
for (;;) {
const char* line_start = m_p;
skip_blanks();
if (at_end()) { m_p = line_start; return; }
if (is_break(*m_p)) { skip_break(); continue; }
if (*m_p == '#') { skip_comment_to_eol(); if (!at_end()) skip_break(); continue; }
// Document markers at column 0 only.
if (line_start == m_p && m_p + 3 <= m_end &&
((memcmp(m_p, "---", 3) == 0) || (memcmp(m_p, "...", 3) == 0))) {
const char* after = m_p + 3;
if (after >= m_end || is_blank(*after) || is_break(*after) || *after == '#') {
m_p = after;
skip_blanks();
if (m_p < m_end && *m_p == '#') skip_comment_to_eol();
if (!at_end()) skip_break();
continue;
}
}
// Real content — restore m_p to the start of this line so the caller
// (peek_indent / parse_block / parse_mapping / parse_sequence) sees
// the line's indentation.
m_p = line_start;
return;
}
}
// Returns the indentation column (count of leading spaces; tabs are not
// valid YAML indentation) of the current line without consuming it.
// Assumes m_p is at the start of a line.
size_t peek_indent() const {
const char* p = m_p;
size_t col = 0;
while (p < m_end && *p == ' ') { ++p; ++col; }
return col;
}
// -------------------------------------------------------------------------
// Scalar filtering (Section 3.x algorithms, adapted from rapidyaml)
// -------------------------------------------------------------------------
// Folds line breaks within a multi-line plain/quoted scalar per the YAML
// spec: every maximal run of N consecutive line breaks folds to a single
// space if N == 1, or to (N-1) literal newlines if N > 1. `lines` are the
// raw (already trailing-whitespace-trimmed for plain, or as-is for quoted)
// per-line spans, i.e. `lines.size() - 1` is the total number of line
// breaks; empty entries indicate runs of consecutive breaks. Plain-scalar
// callers must strip trailing empty entries first (trailing breaks are not
// part of a plain scalar); for quoted scalars leading/trailing runs fold
// by the same rule (e.g. "a\n" -> "a ", "\n\na" -> "\na").
template <class Container>
static void fold_lines(const Container& lines, std::string& out) {
out.clear();
size_t n = lines.size();
if (n == 0) return;
out.append(std::string_view(lines[0]));
size_t i = 1;
while (i < n) {
// One break reaches lines[i]; each *interior* empty entry skipped
// extends the run by one more break. A trailing empty entry is the
// run's end, not an extra break.
size_t breaks = 1;
while (i < n && lines[i].empty() && i + 1 < n) { ++breaks; ++i; }
if (breaks == 1) out.push_back(' ');
else out.append(breaks - 1, '\n');
out.append(std::string_view(lines[i]));
++i;
}
}
// ---- Plain scalar (Section 3.1) -----------------------------------------
//
// Reads a plain (unquoted) scalar starting at m_p, which may span multiple
// lines if continuation lines are indented more than `min_indent` and do
// not look like new block-structure entries. Stops at: ': ' (mapping
// value indicator), ' #' (comment), a line whose indentation is
// <= min_indent, or end of input. Trailing whitespace on each line is
// stripped before folding.
std::string_view read_plain_scalar(size_t min_indent, std::string& scratch) {
std::vector<std::string_view> lines;
const char* line_begin = m_p;
for (;;) {
const char* seg_start = m_p;
const char* last_nonblank = m_p;
bool seg_has_content = false;
bool stop = false;
while (m_p < m_end && !is_break(*m_p)) {
char c = *m_p;
// ': ' or ':' at end of line ends the scalar (mapping key/value sep).
if (c == ':' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1]))) {
stop = true; break;
}
// ' #' (or '#' at start of scalar) starts a comment.
if (c == '#' && (m_p == seg_start || is_blank(m_p[-1]))) {
stop = true; break;
}
if (!is_blank(c)) { last_nonblank = m_p + 1; seg_has_content = true; }
++m_p;
}
if (seg_has_content || !lines.empty())
lines.emplace_back(seg_start, last_nonblank - seg_start);
if (stop) goto done;
if (at_end()) break;
// Peek ahead: is the next line a continuation?
const char* save = m_p;
skip_break();
// Skip blank lines (these become extra folds handled by fold_lines
// via empty entries).
const char* resume = m_p;
for (;;) {
const char* probe = resume;
size_t indent = 0;
while (probe < m_end && *probe == ' ') { ++probe; ++indent; }
if (probe < m_end && is_break(*probe)) {
// blank line
lines.emplace_back();
resume = probe; skip_break_at(resume);
continue;
}
if (probe >= m_end || indent <= min_indent) {
m_p = save;
goto done;
}
// Continuation line.
m_p = probe;
break;
}
}
done:
(void)line_begin;
// Trailing blank lines (from blank-line probing that hit a dedent/EOF)
// are not part of the scalar.
while (!lines.empty() && lines.back().empty()) lines.pop_back();
if (lines.empty()) return std::string_view();
if (lines.size() == 1) return lines[0];
fold_lines(lines, scratch);
return scratch;
}
// Helper for read_plain_scalar: advance `p` past a line break.
static void skip_break_at(const char*& p) {
if (*p == '\r') ++p;
if (*p == '\n') ++p;
}
// ---- Single-quoted scalar (Section 3.2) ----------------------------------
//
// Reads '...' starting at m_p (m_p points at the opening '\''). On return,
// m_p is past the closing quote. `''` is an escaped single quote. Line
// breaks fold per fold_lines; trailing whitespace on each line before a
// fold is stripped.
bool read_single_quoted(std::string& out, size_t /*min_indent*/) {
++m_p; // skip opening quote
std::vector<std::string> lines;
std::string cur;
for (;;) {
if (at_end()) { m_err = "unterminated single-quoted scalar"; return false; }
char c = *m_p;
if (c == '\'') {
if (m_p + 1 < m_end && m_p[1] == '\'') {
cur.push_back('\'');
m_p += 2;
continue;
}
++m_p; // closing quote
break;
}
if (is_break(c)) {
size_t e = cur.size();
while (e > 0 && is_blank(cur[e-1])) --e;
cur.resize(e);
lines.push_back(std::move(cur));
cur.clear();
skip_break();
while (m_p < m_end && *m_p == ' ') ++m_p;
continue;
}
cur.push_back(c);
++m_p;
}
// Trailing blanks before the closing quote are content ('a ' keeps both
// spaces) — only blanks adjacent to a fold are stripped (per-break above).
lines.push_back(std::move(cur));
if (lines.size() == 1) { out = std::move(lines[0]); return true; }
std::vector<std::string_view> views;
views.reserve(lines.size());
for (auto& l : lines) views.emplace_back(l);
fold_lines(views, out);
return true;
}
// ---- Double-quoted scalar (Section 3.3) ----------------------------------
//
// Reads "..." starting at m_p (pointing at opening '"'). Handles full
// escape table and newline folding. On return, m_p is past the closing
// quote.
bool read_double_quoted(std::string& out, size_t min_indent) {
(void)min_indent;
++m_p; // skip opening quote
std::vector<std::string> lines;
std::string cur;
for (;;) {
// SIMD: bulk-append all bytes up to the next '"', '\', '\n', or '\r'.
{
const char* safe = find_dq_special(m_p, m_end);
if (safe > m_p) { cur.append(m_p, safe - m_p); m_p = safe; }
}
if (at_end()) { m_err = "unterminated double-quoted scalar"; return false; }
char c = *m_p;
if (c == '"') { ++m_p; break; }
if (c == '\\') {
++m_p;
if (at_end()) { m_err = "unterminated escape in double-quoted scalar"; return false; }
char e = *m_p;
switch (e) {
case '0': cur.push_back('\0'); ++m_p; break;
case 'a': cur.push_back('\a'); ++m_p; break;
case 'b': cur.push_back('\b'); ++m_p; break;
case 't': case '\t': cur.push_back('\t'); ++m_p; break;
case 'n': cur.push_back('\n'); ++m_p; break;
case 'v': cur.push_back('\v'); ++m_p; break;
case 'f': cur.push_back('\f'); ++m_p; break;
case 'r': cur.push_back('\r'); ++m_p; break;
case 'e': cur.push_back('\x1b'); ++m_p; break;
case ' ': cur.push_back(' '); ++m_p; break;
case '"': cur.push_back('"'); ++m_p; break;
case '/': cur.push_back('/'); ++m_p; break;
case '\\': cur.push_back('\\'); ++m_p; break;
case 'N': append_utf8(cur, 0x85); ++m_p; break; // NEL
case '_': append_utf8(cur, 0xA0); ++m_p; break; // NBSP
case 'L': append_utf8(cur, 0x2028); ++m_p; break; // LS
case 'P': append_utf8(cur, 0x2029); ++m_p; break; // PS
case 'x': { uint32_t cp; if (!read_hex_escape(2, cp)) return false; append_utf8(cur, cp); break; }
case 'u': { uint32_t cp; if (!read_hex_escape(4, cp)) return false; append_utf8(cur, cp); break; }
case 'U': { uint32_t cp; if (!read_hex_escape(8, cp)) return false; append_utf8(cur, cp); break; }
case '\n': case '\r':
// Escaped line break: line-continuation, no fold (consumed below
// along with leading whitespace of the next line).
skip_break();
while (m_p < m_end && is_blank(*m_p)) ++m_p;
continue;
default:
m_err = "invalid escape sequence in double-quoted scalar";
return false;
}
continue;
}
if (is_break(c)) {
// strip trailing blanks already accumulated in cur for this line
size_t e = cur.size();
while (e > 0 && is_blank(cur[e-1])) --e;
cur.resize(e);
lines.push_back(std::move(cur));
cur.clear();
skip_break();
while (m_p < m_end && *m_p == ' ') ++m_p;
continue;
}
// Unreachable: find_dq_special() already stops at any of '"', '\', '\n', '\r'.
cur.push_back(c);
++m_p;
}
// strip trailing blanks of the final segment too (only matters if a fold
// follows immediately before closing quote, which is invalid YAML but be
// lenient)
lines.push_back(std::move(cur));
if (lines.size() == 1) { out = std::move(lines[0]); return true; }
std::vector<std::string_view> views;
views.reserve(lines.size());
for (auto& l : lines) views.emplace_back(l);
fold_lines(views, out);
return true;
}
bool read_hex_escape(int ndigits, uint32_t& out) {
if (m_p + 1 + ndigits > m_end) { m_err = "truncated hex escape"; return false; }
uint32_t v = 0;
for (int i = 1; i <= ndigits; ++i) {
int d = hex_digit_value(static_cast<unsigned char>(m_p[i]));
if (d < 0) { m_err = "invalid hex digit in escape"; return false; }
v = v * 16 + uint32_t(d);
}
m_p += 1 + ndigits;
out = v;
return true;
}
static void append_utf8(std::string& out, uint32_t cp) {
if (cp < 0x80) {
out.push_back(char(cp));
} else if (cp < 0x800) {
out.push_back(char(0xC0 | (cp >> 6)));
out.push_back(char(0x80 | (cp & 0x3F)));
} else if (cp < 0x10000) {
out.push_back(char(0xE0 | (cp >> 12)));
out.push_back(char(0x80 | ((cp >> 6) & 0x3F)));
out.push_back(char(0x80 | (cp & 0x3F)));
} else {
out.push_back(char(0xF0 | (cp >> 18)));
out.push_back(char(0x80 | ((cp >> 12) & 0x3F)));
out.push_back(char(0x80 | ((cp >> 6) & 0x3F)));
out.push_back(char(0x80 | (cp & 0x3F)));
}
}
// ---- Block scalars (| literal, > folded) — Section 8.1 -------------------
//
// m_p points at the '|' or '>' header indicator. `parent_indent` is the
// column of the construct owning the scalar (mapping key column, sequence
// dash column, or 0 for a top-level document): content must be indented
// strictly more. The header may carry a chomping indicator ('-' strip /
// '+' keep / default clip) and an explicit indentation indicator (1-9,
// relative to parent_indent), in either order, followed only by an
// optional comment.
ERL_NIF_TERM read_block_scalar(bool folded, size_t parent_indent) {
++m_p; // consume '|' or '>'
int chomp = 0; // -1 strip, 0 clip, +1 keep
int indicator = -1;
while (m_p < m_end) {
char c = *m_p;
if ((c == '-' || c == '+') && chomp == 0) chomp = (c == '-') ? -1 : 1;
else if (c >= '1' && c <= '9' && indicator < 0) indicator = c - '0';
else break;
++m_p;
}
skip_blanks();
if (!at_end() && *m_p == '#') skip_comment_to_eol();
if (!at_end() && !is_break(*m_p)) { m_err = "invalid block scalar header"; return 0; }
if (!at_end()) skip_break();
size_t indent = indicator > 0 ? parent_indent + size_t(indicator) : 0;
bool detected = indicator > 0;
std::vector<std::string_view> lines;
size_t breaks_after = 0; // line breaks since the last non-empty content line
for (;;) {
if (at_end()) break;
const char* line_begin = m_p;
const char* q = line_begin;
size_t s = 0;
while (q < m_end && *q == ' ') { ++q; ++s; }
const char* r = q;
while (r < m_end && is_blank(*r)) ++r; // tabs after the space indent
if (r >= m_end || is_break(*r)) {
// Blank line. Whitespace beyond the indent is content ("x\n \n"
// at indent 2 has a " " content line); otherwise the line is empty.
if (detected && size_t(r - line_begin) > indent) {
lines.emplace_back(line_begin + indent, size_t(r - line_begin) - indent);
breaks_after = 0;
} else {
lines.emplace_back();
}
m_p = r;
if (!at_end()) { skip_break(); ++breaks_after; }
continue;
}
// Non-blank line: detect / verify indentation.
if (!detected) {
if (s <= parent_indent) break; // not part of this scalar
indent = s;
detected = true;
} else if (s < indent) {
break; // dedent ends the scalar
}
const char* eol = r;
while (eol < m_end && !is_break(*eol)) ++eol;
// Content is verbatim from the indent column, incl. extra leading
// spaces (more-indented lines) and trailing blanks.
lines.emplace_back(line_begin + indent, size_t(eol - line_begin) - indent);
breaks_after = 0;
m_p = eol;
if (!at_end()) { skip_break(); ++breaks_after; }
}
while (!lines.empty() && lines.back().empty()) lines.pop_back();
std::string body;
if (!folded) {
// Literal: verbatim lines joined by single newlines.
for (size_t i = 0; i < lines.size(); ++i) {
if (i) body.push_back('\n');
body.append(lines[i]);
}
} else {
// Folded: like plain-scalar folding, except breaks adjacent to
// more-indented lines (leading whitespace after the indent) are
// literal, and so are the empty lines between them.
size_t i = 0, n = lines.size();
while (i < n && lines[i].empty()) { body.push_back('\n'); ++i; }
if (i < n) {
body.append(lines[i]);
bool prev_more = is_blank(lines[i].front());
++i;
while (i < n) {
size_t k = 0;
while (i + k < n && lines[i + k].empty()) ++k;
i += k;
bool cur_more = is_blank(lines[i].front());
// k empty entries bounded by content = k+1 breaks. Literal next
// to more-indented lines; folded otherwise (1 break -> space,
// k+1 breaks -> k newlines).
if (prev_more || cur_more) body.append(k + 1, '\n');
else if (k == 0) body.push_back(' ');
else body.append(k, '\n');
body.append(lines[i]);
prev_more = cur_more;
++i;
}
}
}
// Chomping: strip drops all trailing breaks, clip keeps at most one,
// keep retains them all.
if (chomp > 0)
body.append(breaks_after, '\n');
else if (chomp == 0 && breaks_after > 0 && !body.empty())
body.push_back('\n');
return make_binary(m_env, body);
}
// -------------------------------------------------------------------------
// Anchors and aliases (&name / *name)
// -------------------------------------------------------------------------
// Reads the name following an '&' or '*' indicator at m_p. Names end at
// whitespace, a line break, or a flow indicator.
bool read_anchor_name(std::string& name) {
bool alias = (*m_p == '*');
++m_p;
const char* s = m_p;
while (m_p < m_end && !is_blank(*m_p) && !is_break(*m_p) && !is_flow_indicator(*m_p))
++m_p;
if (m_p == s) {
m_err = alias ? "empty alias name" : "empty anchor name";
return false;
}
name.assign(s, size_t(m_p - s));
return true;
}
// Resolves a '*alias' at m_p to its anchored term. An alias to an anchor
// whose node hasn't completed yet (i.e. a cycle — unrepresentable as an
// Erlang term) is reported as undefined.
ERL_NIF_TERM resolve_alias() {
std::string name;
if (!read_anchor_name(name)) return 0;
auto it = m_anchors.find(name);
if (it == m_anchors.end()) {
m_err = "undefined alias '" + name + "'";
return 0;
}
return it->second;
}
// -------------------------------------------------------------------------
// Implicit scalar typing (Section 1.3 / YAML 1.2 core schema)
// -------------------------------------------------------------------------
ERL_NIF_TERM resolve_plain_scalar(std::string_view s) {
if (s.empty() || s == "~" || s == "null" || s == "Null" || s == "NULL")
return m_opts.null_term;
if (s == "true" || s == "True" || s == "TRUE") return AM_TRUE;
if (s == "false" || s == "False" || s == "FALSE") return AM_FALSE;
if (m_opts.yaml_1_1_bools) {
if (s == "yes" || s == "Yes" || s == "YES" ||
s == "on" || s == "On" || s == "ON")
return AM_TRUE;
if (s == "no" || s == "No" || s == "NO" ||
s == "off" || s == "Off" || s == "OFF")
return AM_FALSE;
}
if (s == ".inf" || s == ".Inf" || s == ".INF" || s == "+.inf" || s == "+.Inf" || s == "+.INF")
return AM_INFINITY;
if (s == "-.inf" || s == "-.Inf" || s == "-.INF")
return AM_NEG_INFINITY;
if (s == ".nan" || s == ".NaN" || s == ".NAN")
return AM_NAN;
if (ERL_NIF_TERM num = try_parse_number(s)) return num;
return make_span_term(m_env, m_input_bin, m_beg, m_end, s, m_opts.copy_strings);
}
// Returns 0 if `s` doesn't look like a YAML core-schema int/float.
ERL_NIF_TERM try_parse_number(std::string_view s) {
if (s.empty()) return 0;
const char* p = s.data();
const char* end = p + s.size();
const char* start = p;
bool neg = false;
if (*p == '+' || *p == '-') { neg = (*p == '-'); ++p; }
if (p == end) return 0;
// Hex / octal: 0x... / 0o...
if (*p == '0' && p + 1 < end && (p[1] == 'x' || p[1] == 'X')) {
return parse_radix_int(p + 2, end, 16, neg);
}
if (*p == '0' && p + 1 < end && (p[1] == 'o' || p[1] == 'O')) {
return parse_radix_int(p + 2, end, 8, neg);
}
// Scan digits / one dot / exponent to determine int vs float.
const char* q = p;
bool has_digit = false, has_dot = false, has_exp = false;
while (q < end) {
char c = *q;
if (c >= '0' && c <= '9') { has_digit = true; ++q; continue; }
if (c == '.' && !has_dot && !has_exp) { has_dot = true; ++q; continue; }
if ((c == 'e' || c == 'E') && has_digit && !has_exp) {
has_exp = true; ++q;
if (q < end && (*q == '+' || *q == '-')) ++q;
continue;
}
return 0; // not a number
}
if (!has_digit) return 0;
if (!has_dot && !has_exp) {
// Plain integer.
if (neg) {
int64_t v = 0;
auto [ep, ec] = std::from_chars(start, end, v);
if (ec == std::errc{} && ep == end) return enif_make_int64(m_env, v);
} else {
uint64_t v = 0;
auto [ep, ec] = std::from_chars(start, end, v);
if (ec == std::errc{} && ep == end)
return v <= uint64_t(INT64_MAX) ? enif_make_int64(m_env, int64_t(v))
: enif_make_uint64(m_env, v);
}
ERL_NIF_TERM r = glz::BigInt::decode(m_env, start, end);
return r ? r : (ERL_NIF_TERM)0;
}
double d;
auto [ep, ec] = glz::fast_float::from_chars(start, end, d);
if (ec == std::errc{} && ep == end)
return enif_make_double(m_env, d);
return 0;
}
ERL_NIF_TERM parse_radix_int(const char* p, const char* end, int radix, bool neg) {
if (p >= end) return 0;
for (const char* q = p; q < end; ++q) {
char c = *q;
bool ok = (radix == 16) ? ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
: (c >= '0' && c <= '7');
if (!ok) return 0;
}
uint64_t v = 0;
auto [ep, ec] = std::from_chars(p, end, v, radix);
if (ec != std::errc{} || ep != end) return 0;
int64_t sv = neg ? -int64_t(v) : int64_t(v);
return enif_make_int64(m_env, sv);
}
// -------------------------------------------------------------------------
// Block-style parsing
// -------------------------------------------------------------------------
// True iff `s` is a sub-span of the original input buffer — i.e. safe to
// retain a pointer into (KeyCache::insert) rather than a transient local
// buffer (a quoted-scalar `out` or a fold/unescape `scratch`) that is
// destroyed when the caller returns.
bool is_input_span(std::string_view s) const {
return s.data() >= m_beg && s.data() + s.size() <= m_end;
}
// Make a key term honoring hdr_atom / hdr_existing_atom. Mirrors
// JSONDecoder::make_key_term in glazer_json.hpp: the key cache only retains
// keys backed by the original input (mirrors JSON's has_escape bypass) —
// `s` may instead be a quoted-scalar or line-folded scratch buffer local
// to the caller, which must never be cached by pointer.
ERL_NIF_TERM make_key_term(std::string_view s) {
if (m_opts.hdr_atom)
return enif_make_atom_len(m_env, s.data(), s.size());
if (m_opts.hdr_existing_atom) {
ERL_NIF_TERM t;
return enif_make_existing_atom_len(m_env, s.data(), s.size(), &t, ERL_NIF_LATIN1)
? t : make_span_term(m_env, m_input_bin, m_beg, m_end, s, m_opts.copy_strings);
}
if (m_use_key_cache && is_input_span(s)) {
uint32_t h = KeyCache::hash_of(s.data(), s.size());
if (ERL_NIF_TERM cached = m_key_cache.lookup(s.data(), s.size(), h))
return cached;
auto term = make_span_term(m_env, m_input_bin, m_beg, m_end, s, m_opts.copy_strings);
m_key_cache.insert(s.data(), s.size(), h, term);
return term;
}
return make_span_term(m_env, m_input_bin, m_beg, m_end, s, m_opts.copy_strings);
}
// Parses a single scalar token at the current position (plain, single- or
// double-quoted), returning the resolved Erlang term. `min_indent` bounds
// multi-line plain scalar continuations. Sets m_err on failure (returns 0).
ERL_NIF_TERM parse_scalar(size_t min_indent) {
if (at_end()) return m_opts.null_term;
char c = *m_p;
std::string scratch;
if (c == '\'') {
std::string out;
if (!read_single_quoted(out, min_indent)) return 0;
return make_binary(m_env, out);
}
if (c == '"') {
std::string out;
if (!read_double_quoted(out, min_indent)) return 0;
return make_binary(m_env, out);
}
std::string_view s = read_plain_scalar(min_indent, scratch);
return resolve_plain_scalar(s);
}
// After reading a `key:` or `- ` marker, decide what follows on the rest
// of this line:
// - nothing / only a comment -> value is on subsequent indented lines
// (nested block) or is null (empty)
// - a scalar / nested flow value -> parse it inline
// `min_indent` is the indent column of the current container (mapping or
// sequence) — used to bound plain scalar continuations.
ERL_NIF_TERM parse_node(size_t min_indent) {
skip_blanks();
if (at_end() || is_break(*m_p) || *m_p == '#') {
// Empty inline value -> look for a nested block on following lines.
const char* save = m_p;
if (!at_end() && *m_p == '#') skip_comment_to_eol();
if (!at_end()) skip_break();
skip_blank_and_comment_lines();
if (at_end()) { m_p = save; return m_opts.null_term; }
size_t next_indent = peek_indent();
if (next_indent > min_indent) {
// A block scalar header may sit on its own line ("a:\n |\n x").
const char* q = m_p + next_indent;
if (q < m_end && (*q == '|' || *q == '>')) {
m_p = q;
return read_block_scalar(*q == '>', min_indent);
}
return parse_block(next_indent);
}
// A block sequence is allowed at the same indentation as the mapping
// key that owns it (e.g. "imp:\n- id: x"), per spec.
if (next_indent == min_indent) {
const char* q = m_p + next_indent;
if (q < m_end && *q == '-' && (q + 1 >= m_end || is_blank(q[1]) || is_break(q[1])))
return parse_sequence(next_indent);
}
m_p = save;
return m_opts.null_term;
}
// Anchor: '&name' binds the node that follows; alias: '*name' reuses it.
if (*m_p == '&') {
std::string name;
if (!read_anchor_name(name)) return 0;
ERL_NIF_TERM v = parse_node(min_indent);
if (!v) return 0;
m_anchors[name] = v;
return v;
}
if (*m_p == '*')
return resolve_alias();
// Block scalar ("a: |", "- >-", ...).
if (*m_p == '|' || *m_p == '>')
return read_block_scalar(*m_p == '>', min_indent);
// Inline flow collection ("a: [1, 2]" / "- {k: v}").
if (*m_p == '[' || *m_p == '{') {
ERL_NIF_TERM v = parse_flow_node();
if (!v) return 0;
if (!check_flow_line_end()) return 0;
return v;
}
// Inline value on the same line: a scalar (plain/quoted), a nested block
// sequence ("- item"), or a nested block mapping ("key: value" — common
// after a sequence dash, e.g. "- a: 1"). All of these start a nested
// block at the current column.
if (*m_p == '-' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1]))) {
size_t col = size_t(m_p - line_start(m_p));
return parse_block(col, /*at_line_start=*/false);
}
if (looks_like_mapping_line()) {
size_t col = size_t(m_p - line_start(m_p));
return parse_block(col, /*at_line_start=*/false);
}
return parse_scalar(min_indent);
}
// Returns a pointer to the start of the line containing `p`.
const char* line_start(const char* p) const {
while (p > m_beg && p[-1] != '\n') --p;
return p;
}
// Parses a block node (mapping or sequence) whose entries are indented
// exactly `indent` columns. Dispatches based on the first non-blank
// character of the current line.
//
// If `at_line_start` is false, `m_p` is already positioned exactly at
// column `indent` of the *current* line (e.g. right after a sequence
// dash's "- " prefix, as in "- a: 1" or "- - 1") — the usual
// skip-to-line-start / indentation check is skipped for the first entry.
ERL_NIF_TERM parse_block(size_t indent, bool at_line_start = true) {
DepthGuard guard(this);
if (!guard.ok()) { m_err = "exceeded maximum nesting depth"; return 0; }
const char* p;
if (at_line_start) {
skip_blank_and_comment_lines();
if (at_end()) return m_opts.null_term;
if (peek_indent() != indent) return m_opts.null_term;
p = m_p + indent;
} else {
p = m_p;
}
// A flow collection on its own line ("a:\n [1, 2]").
if (p < m_end && (*p == '[' || *p == '{')) {
m_p = p;
ERL_NIF_TERM v = parse_flow_node();
if (!v) return 0;
if (!check_flow_line_end()) return 0;
return v;
}
if (p < m_end && *p == '-' && (p + 1 >= m_end || is_blank(p[1]) || is_break(p[1])))
return parse_sequence(indent, at_line_start);
return parse_mapping(indent, at_line_start);
}
// Parses a block sequence: a run of `- item` lines all indented at `indent`.
// See parse_block for the meaning of `at_line_start`.
ERL_NIF_TERM parse_sequence(size_t indent, bool at_line_start = true) {
SmallTermVec<16> items;
bool first = !at_line_start;
for (;;) {
const char* line_start_p = m_p;
if (!first) {
skip_blank_and_comment_lines();
line_start_p = m_p;
if (at_end() || peek_indent() != indent) break;
m_p += indent;
}
first = false;
const char* p = m_p;
if (!(p < m_end && *p == '-' && (p + 1 >= m_end || is_blank(p[1]) || is_break(p[1])))) {
m_p = line_start_p;
break;
}
m_p = p + 1; // consume '-'
skip_blanks();
// The item's indentation bound is the dash column: continuation lines
// of plain scalars, nested blocks on following lines, and block
// scalar content all only need to be indented past the dash
// ("- foo\n bar" folds; "- |\n x" is content; "-\n a: 1" nests).
ERL_NIF_TERM v = parse_node(indent);
if (!v) return 0;
items.push_back(v);
}
return enif_make_list_from_array(m_env, items.data(), unsigned(items.size()));
}
// Parses a block mapping: a run of `key: value` lines all indented at
// `indent`. See parse_block for the meaning of `at_line_start`.
ERL_NIF_TERM parse_mapping(size_t indent, bool at_line_start = true) {
SmallTermVec<16> ks, vs;
bool first = !at_line_start;
for (;;) {
const char* line_start_p = m_p;
if (!first) {
skip_blank_and_comment_lines();
line_start_p = m_p;
if (at_end() || peek_indent() != indent) break;
m_p += indent;
}
first = false;
if (m_p < m_end && *m_p == '-' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1])))
{ m_p = line_start_p; break; } // sequence item at this indent — not part of this mapping
// Parse the key (plain or quoted scalar, up to ':').
ERL_NIF_TERM key_term;
std::string scratch;
if (*m_p == '[' || *m_p == '{') {
m_err = "flow collection keys are not supported";
return 0;
}
if (*m_p == '\'') {
std::string out;
if (!read_single_quoted(out, indent)) return 0;
key_term = make_key_term(out);
} else if (*m_p == '"') {
std::string out;
if (!read_double_quoted(out, indent)) return 0;
key_term = make_key_term(out);
} else {
std::string_view k = read_plain_key(indent);
if (m_err.size()) return 0;
key_term = make_key_term(k);
}
skip_blanks();
if (at_end() || *m_p != ':') { m_err = "expected ':' after mapping key"; return 0; }
++m_p;
ERL_NIF_TERM val = parse_node(indent);
if (!val) return 0;
ks.push_back(key_term);
vs.push_back(val);
}
[[maybe_unused]] auto map = vs.to_erl_map<true>(m_env, ks);
assert(map != 0);
return map;
}
// Reads a plain scalar that is to be used as a mapping key: stops at the
// first ':' followed by space/EOL/EOF (the key/value separator), without
// the multi-line folding plain scalars otherwise allow (mapping keys are
// single-line in block context for Step 1).
std::string_view read_plain_key(size_t /*indent*/) {
const char* seg_start = m_p;
const char* last_nonblank = m_p;
while (m_p < m_end && !is_break(*m_p)) {
char c = *m_p;
if (c == ':' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1])))
break;
if (c == '#' && (m_p == seg_start || is_blank(m_p[-1])))
break;
if (!is_blank(c)) last_nonblank = m_p + 1;
++m_p;
}
if (seg_start == last_nonblank) { m_err = "empty mapping key"; }
return std::string_view(seg_start, last_nonblank - seg_start);
}
// -------------------------------------------------------------------------
// Flow-style parsing ({...} mappings, [...] sequences)
// -------------------------------------------------------------------------
// Skips whitespace inside a flow collection: blanks, line breaks, and
// comments (a '#' preceded by whitespace or at the start of input).
void skip_flow_ws() {
for (;;) {
skip_blanks();
if (at_end()) return;
char c = *m_p;
if (is_break(c)) { skip_break(); continue; }
if (c == '#' && (m_p == m_beg || is_blank(m_p[-1]) || is_break(m_p[-1]))) {
skip_comment_to_eol();
continue;
}
return;
}
}
// Reads a plain scalar in flow context. In addition to the block-context
// terminators (': ', ' #'), flow indicators (',', '[', ']', '{', '}')
// terminate the scalar, and ':' is a terminator when followed by a flow
// indicator as well as by whitespace/end (so `a:1` stays one scalar, per
// spec). May span lines, folded like block plain scalars.
std::string_view read_flow_plain_scalar(std::string& scratch) {
std::vector<std::string_view> lines;
for (;;) {
const char* seg_start = m_p;
const char* last_nonblank = m_p;
bool seg_has_content = false;
bool stop = false;
while (m_p < m_end && !is_break(*m_p)) {
char c = *m_p;
if (is_flow_indicator(c)) { stop = true; break; }
if (c == ':' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1]) ||
is_flow_indicator(m_p[1]))) {
stop = true; break;
}
if (c == '#' && (m_p == seg_start || is_blank(m_p[-1]))) {
stop = true; break;
}
if (!is_blank(c)) { last_nonblank = m_p + 1; seg_has_content = true; }
++m_p;
}
if (seg_has_content)
lines.emplace_back(seg_start, last_nonblank - seg_start);
if (stop || at_end()) break;
// Line break inside the scalar: fold. Blank lines become empty entries.
skip_break();
for (;;) {
const char* probe = m_p;
while (probe < m_end && is_blank(*probe)) ++probe;
if (probe < m_end && is_break(*probe)) {
lines.emplace_back();
m_p = probe;
skip_break();
continue;
}
m_p = probe;
break;
}
if (at_end()) break;
}
while (!lines.empty() && lines.back().empty()) lines.pop_back();
if (lines.empty()) return std::string_view();
if (lines.size() == 1) return lines[0];
fold_lines(lines, scratch);
return scratch;
}
// Verifies the start of a flow scalar isn't a block construct (block
// collections and block scalars are not allowed inside flow collections
// per spec — error rather than mis-parse). Returns false and sets m_err
// if it is.
bool check_no_block_in_flow() {
char c = *m_p;
if ((c == '-' || c == '?') &&
(m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1]))) [[unlikely]] {
m_err = "block entries are not allowed in flow context";
return false;
}
if (c != '|' && c != '>') [[likely]]
return true;
m_err = "block scalars are not allowed in flow context";
return false;
}
// Parses a single node in flow context: a nested flow collection, a quoted
// scalar, or a plain scalar. m_p may be at leading whitespace.
ERL_NIF_TERM parse_flow_node() {
skip_flow_ws();
if (at_end()) [[unlikely]] { m_err = "unexpected end of input in flow context"; return 0; }
char c = *m_p;
if (c == '&') {
std::string name;
if (!read_anchor_name(name)) return 0;
ERL_NIF_TERM v = parse_flow_node();
if (!v) return 0;
m_anchors[name] = v;
return v;
}
if (c == '*') return resolve_alias();
if (c == '[') return parse_flow_sequence();
if (c == '{') return parse_flow_mapping();
if (c == '\'') {
std::string out;
if (!read_single_quoted(out, 0)) return 0;
return make_binary(m_env, out);
}
if (c == '"') {
std::string out;
if (!read_double_quoted(out, 0)) return 0;
return make_binary(m_env, out);
}
if (!check_no_block_in_flow()) return 0;
std::string scratch;
std::string_view s = read_flow_plain_scalar(scratch);
return resolve_plain_scalar(s);
}
// Parses one flow-sequence entry. Supports the single-pair mapping
// shorthand `[key: value, ...]`, where the entry decodes to a one-entry
// map (PyYAML-compatible: `[a: 1, b: 2]` -> [#{a=>1}, #{b=>2}]).
ERL_NIF_TERM parse_flow_seq_entry() {
char c = *m_p;
// Anchored or aliased entries take the plain-node path (an anchored
// single-pair `[&a k: v]` is not supported).
if (c == '[' || c == '{' || c == '&' || c == '*') return parse_flow_node();
bool quoted = (c == '\'' || c == '"');
std::string qout;
std::string scratch;
std::string_view s;
if (c == '\'') {
if (!read_single_quoted(qout, 0)) return 0;
s = qout;
} else if (c == '"') {
if (!read_double_quoted(qout, 0)) return 0;
s = qout;
} else {
if (!check_no_block_in_flow()) return 0;
s = read_flow_plain_scalar(scratch);
}
if (quoted) skip_blanks();
if (m_p < m_end && *m_p == ':' &&
(quoted || m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1]) ||
is_flow_indicator(m_p[1]))) {
// Single-pair mapping entry.
++m_p;
skip_flow_ws();
ERL_NIF_TERM val = m_opts.null_term;
if (at_end()) [[unlikely]] { m_err = "unterminated flow sequence"; return 0; }
if (*m_p != ',' && *m_p != ']') {
val = parse_flow_node();
if (!val) return 0;
}
ERL_NIF_TERM map = enif_make_new_map(m_env), next;
enif_make_map_put(m_env, map, make_key_term(s), val, &next);
return next;
}
return quoted ? make_binary(m_env, s) : resolve_plain_scalar(s);
}
// Parses a flow sequence `[a, b, ...]`. m_p is at the opening '['; on
// success m_p is past the closing ']'. Trailing commas are allowed.
ERL_NIF_TERM parse_flow_sequence() {
DepthGuard guard(this);
if (!guard.ok()) [[unlikely]] { m_err = "exceeded maximum nesting depth"; return 0; }
++m_p; // consume '['
SmallTermVec<16> items;
for (;;) {
skip_flow_ws();
if (at_end()) [[unlikely]] { m_err = "unterminated flow sequence"; return 0; }
if (*m_p == ']') { ++m_p; break; }
if (*m_p == ',') [[unlikely]] { m_err = "unexpected ',' in flow sequence"; return 0; }
ERL_NIF_TERM v = parse_flow_seq_entry();
if (!v) return 0;
items.push_back(v);
skip_flow_ws();
if (at_end()) [[unlikely]] { m_err = "unterminated flow sequence"; return 0; }
if (*m_p == ',') { ++m_p; continue; }
if (*m_p == ']') [[likely]] { ++m_p; break; }
m_err = "expected ',' or ']' in flow sequence";
return 0;
}
return enif_make_list_from_array(m_env, items.data(), unsigned(items.size()));
}
// Parses a flow mapping `{k: v, ...}`. m_p is at the opening '{'; on
// success m_p is past the closing '}'. Trailing commas are allowed; a key
// without ': value' gets a null value (`{a, b}`).
ERL_NIF_TERM parse_flow_mapping() {
DepthGuard guard(this);
if (!guard.ok()) [[unlikely]] { m_err = "exceeded maximum nesting depth"; return 0; }
++m_p; // consume '{'
SmallTermVec<16> ks, vs;
for (;;) {
skip_flow_ws();
if (at_end()) [[unlikely]] { m_err = "unterminated flow mapping"; return 0; }
if (*m_p == '}') { ++m_p; break; }
if (*m_p == ',') [[unlikely]] { m_err = "unexpected ',' in flow mapping"; return 0; }
// Key: a plain or quoted scalar (collection/complex keys unsupported).
char c = *m_p;
ERL_NIF_TERM key_term;
bool quoted = (c == '\'' || c == '"');
if (c == '[' || c == '{') [[unlikely]] {
m_err = "flow collection keys are not supported";
return 0;
} else if (c == '\'') {
std::string out;
if (!read_single_quoted(out, 0)) return 0;
key_term = make_key_term(out);
} else if (c == '"') {
std::string out;
if (!read_double_quoted(out, 0)) return 0;
key_term = make_key_term(out);
} else {
if (!check_no_block_in_flow()) return 0;
std::string scratch;
std::string_view s = read_flow_plain_scalar(scratch);
if (s.empty() && (at_end() || *m_p != ':')) [[unlikely]] {
m_err = "expected mapping key in flow mapping";
return 0;
}
key_term = make_key_term(s);
}
if (quoted) skip_blanks();
ERL_NIF_TERM val = m_opts.null_term;
if (m_p < m_end && *m_p == ':') {
++m_p;
skip_flow_ws();
if (at_end()) [[unlikely]] { m_err = "unterminated flow mapping"; return 0; }
if (*m_p != ',' && *m_p != '}') {
val = parse_flow_node();
if (!val) return 0;
}
}
ks.push_back(key_term);
vs.push_back(val);
skip_flow_ws();
if (at_end()) [[unlikely]] { m_err = "unterminated flow mapping"; return 0; }
if (*m_p == ',') { ++m_p; continue; }
if (*m_p == '}') { ++m_p; break; }
m_err = "expected ',' or '}' in flow mapping";
return 0;
}
[[maybe_unused]] auto map = vs.to_erl_map<true>(m_env, ks);
assert(map);
return map;
}
// After an inline flow collection in block context, only blanks and a
// comment may remain on the line.
bool check_flow_line_end() {
skip_blanks();
if (!at_end() && *m_p == '#') skip_comment_to_eol();
if (at_end() || is_break(*m_p)) [[likely]]
return true;
m_err = "unexpected content after flow collection";
return false;
}
// -------------------------------------------------------------------------
// Entry point
// -------------------------------------------------------------------------
std::tuple<bool, ERL_NIF_TERM> decode() {
skip_blank_and_comment_lines();
if (at_end()) [[unlikely]]
return std::make_tuple(true, m_opts.null_term);
size_t indent = peek_indent();
ERL_NIF_TERM result;
const char* p = m_p + indent;
if (p < m_end && (*p == '&' || *p == '*')) {
// Root-node anchors are useless in single-document mode (nothing can
// alias the root before it completes) — reject rather than mis-parse.
m_err = "top-level anchors/aliases are not supported";
result = 0;
}
else if (p < m_end && (*p == '|' || *p == '>')) {
// Top-level block scalar document.
m_p = p;
result = read_block_scalar(*p == '>', indent);
}
else if (p < m_end && (*p == '[' || *p == '{')) {
// Top-level flow document.
m_p = p;
result = parse_flow_node();
}
else if (p < m_end && *p == '-' && (p + 1 >= m_end || is_blank(p[1]) || is_break(p[1])))
result = parse_sequence(indent);
else {
// Could be a single top-level scalar document.
m_p += indent;
const char* save = m_p;
// Try mapping first: if the line contains "key:" at top level.
if (looks_like_mapping_line()) {
m_p = save;
result = parse_mapping(indent, /*at_line_start=*/false);
} else {
result = parse_scalar(indent);
skip_blank_and_comment_lines();
}
}
if (!result) [[unlikely]] {
std::string msg = m_err.empty()
? ("YAML 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));
}
skip_blank_and_comment_lines();
if (!at_end()) [[unlikely]] {
std::string msg = "trailing content at offset " + std::to_string(m_p - m_beg);
return std::make_tuple(false, make_binary(m_env, msg));
}
return std::make_tuple(true, result);
}
// Heuristic: does the current line, read as a plain/quoted scalar key,
// contain a top-level "key:" / "key: value" separator (i.e. is this a
// mapping key line)? Restores m_p.
bool looks_like_mapping_line() {
const char* save = m_p;
bool found = false;
if (m_p < m_end && (*m_p == '\'' || *m_p == '"')) {
char q = *m_p; ++m_p;
while (m_p < m_end && !is_break(*m_p)) {
if (*m_p == q) {
if (q == '\'' && m_p + 1 < m_end && m_p[1] == '\'') { m_p += 2; continue; }
++m_p;
break;
}
++m_p;
}
skip_blanks();
if (m_p < m_end && *m_p == ':' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1])))
found = true;
} else {
const char* seg_start = m_p;
while (m_p < m_end && !is_break(*m_p)) {
char c = *m_p;
if (c == ':' && (m_p + 1 >= m_end || is_blank(m_p[1]) || is_break(m_p[1]))) {
found = true; break;
}
if (c == '#' && (m_p == seg_start || is_blank(m_p[-1]))) break;
++m_p;
}
}
m_p = save;
return found;
}
};
//-----------------------------------------------------------------------------
// Erlang-term -> YAML encoder (block style, 2-space indentation)
//
// Mirrors the JSON encoder's term-dispatch shape, but writes block-style
// YAML: nested mappings/sequences are indented two spaces per level, with
// sequences placed at the *same* indentation as the mapping key that owns
// them (the style produced by PyYAML/libyaml and accepted by the decoder
// above). Empty maps/sequences fall back to flow style (`{}` / `[]`) since
// block style has no representation for them.
//
// Scalars are emitted in plain style where unambiguous, single-quoted where
// quoting is needed but the content is plain UTF-8 with no control
// characters, and double-quoted (with full escaping) otherwise.
//-----------------------------------------------------------------------------
struct YAMLEncoder {
ErlNifEnv* m_env;
const YAMLEncodeOpts& m_opts;
OutBuf& m_out;
char m_atom_buf[256];
const char* m_err;
ERL_NIF_TERM m_err_term;
// -------------------------------------------------------------------------
// Scalar string analysis / emission
// -------------------------------------------------------------------------
// Returns true if `s`, written unquoted, would be re-read by the core
// schema as something other than a string (null/bool/number/special
// float), per resolve_plain_scalar's rules in the decoder above.
//
// Dispatches on the first character first: every literal this function
// recognizes (and every numeric scalar) starts with one of
// "~ntfyYNoO.+-0123456789", so plain words like "Alice" or "NYC" bail out
// after a single branch instead of running the full literal/number checks.
static bool looks_like_non_string_scalar(std::string_view s) {
if (s.empty()) return true;
auto c = s.front();
switch (c) {
case '~':
return s == "~";
case 'n': return s == "null" || s == "no";
case 'N': return s == "Null" || s == "NULL" || s == "No" || s == "NO";
case 't': return s == "true";
case 'T': return s == "True" || s == "TRUE";
case 'f': return s == "false";
case 'F': return s == "False" || s == "FALSE";
case 'y': return s == "yes";
case 'Y': return s == "Yes" || s == "YES";
case 'O': return s == "On" || s == "ON" || s == "Off" || s == "OFF";
case 'o': return s == "on" || s == "off";
case '.':
return s == ".inf" || s == ".Inf" || s == ".INF" ||
s == ".nan" || s == ".NaN" || s == ".NAN";
case '+': case '-':
if (s == "+.inf" || s == "+.Inf" || s == "+.INF" ||
s == "-.inf" || s == "-.Inf" || s == "-.INF")
return true;
break;
default:
// Is c in range '0'-'9'?
if ((unsigned char)(c - '0') <= 9)
break;
return false;
}
// Looks like a YAML core-schema int/float (decimal, hex, octal)?
const char* p = s.data();
const char* end = p + s.size();
if (*p == '+' || *p == '-') ++p;
if (p == end) return false;
if (*p == '0' && p + 1 < end && (p[1] == 'x' || p[1] == 'X' || p[1] == 'o' || p[1] == 'O')) {
for (const char* q = p + 2; q < end; ++q)
if (!std::isxdigit((unsigned char)*q)) return false;
return p + 2 < end;
}
bool has_digit = false, has_dot = false, has_exp = false;
for (const char* q = p; q < end; ++q) {
char c = *q;
if (c >= '0' && c <= '9') { has_digit = true; continue; }
if (c == '.' && !has_dot && !has_exp) { has_dot = true; continue; }
if ((c == 'e' || c == 'E') && has_digit && !has_exp) {
has_exp = true;
if (q + 1 < end && (q[1] == '+' || q[1] == '-')) ++q;
continue;
}
return false;
}
return has_digit;
}
static inline bool is_indicator_char(char c) {
static constexpr auto table = [] {
std::array<bool, 256> t{};
for (char ind : {'-','?',':',',','[',']','{','}','#','&','*','!','|','>','\'','"','%','@','`'})
t[(unsigned char)ind] = true;
return t;
}();
return table[(unsigned char)c];
}
// Returns true if `s` contains a byte that forces double-quoting (control
// characters other than plain printable text — single-quoted scalars
// cannot represent these). Uses SIMD to scan for c < 0x20 (unsigned)
// via the bias trick: (c ^ 0x80) < 0xA0 in signed comparison.
static bool needs_double_quote(std::string_view s) noexcept {
const char* p = s.data();
const char* end = p + s.size();
#if defined(__AVX2__)
{
const __m256i vbias = _mm256_set1_epi8(-128);
const __m256i vcmp = _mm256_set1_epi8(-96); // 0xA0 as signed
while (p + 32 <= end) {
__m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(p));
uint32_t mask = (uint32_t)_mm256_movemask_epi8(
_mm256_cmpgt_epi8(vcmp, _mm256_xor_si256(v, vbias)));
if (mask) return true;
p += 32;
}
}
#endif
#if defined(__SSE2__)
{
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));
unsigned mask = (unsigned)_mm_movemask_epi8(
_mm_cmpgt_epi8(vcmp, _mm_xor_si128(v, vbias)));
if (mask) return true;
p += 16;
}
}
#endif
while (p < end) { if ((unsigned char)*p < 0x20) return true; ++p; }
return false;
}
// Returns true if `s` can be written as a YAML plain scalar without any
// quoting, in the contexts this encoder uses (block mapping value / block
// sequence item, single-line content only).
static bool is_safe_plain_scalar(std::string_view s) {
if (s.empty()) return false;
if (looks_like_non_string_scalar(s)) return false;
char first = s.front();
if (is_indicator_char(first)) return false;
// A leading '-', '?', ':' is always disallowed above for simplicity
// (matches common emitters and avoids edge cases).
if (first == ' ' || s.back() == ' ') return false;
// Single pass: control chars force double-quoting; ": " / trailing ':'
// and " #" are plain-scalar-ending sequences.
for (size_t i = 0; i < s.size(); ++i) {
unsigned char c = s[i];
if (c < 0x20) return false;
if (c == ':' && (i + 1 == s.size() || s[i+1] == ' ')) return false;
if (c == '#' && i > 0 && s[i-1] == ' ') return false;
}
return true;
}
// Emit `s` single-quoted, doubling embedded single quotes.
// SIMD locates each '\'' for bulk-copy runs between them.
void emit_single_quoted(std::string_view s) {
m_out.push('\'');
const char* p = s.data();
const char* end = p + s.size();
for (;;) {
const char* q = find_byte(p, end, '\'');
m_out.push(p, q - p); // bulk-copy bytes up to (not including) the quote
if (q >= end) break;
m_out.push("''", 2); // double the embedded single quote
p = q + 1;
}
m_out.push('\'');
}
// Emit `s` double-quoted with full C-style escaping (the JSON-compatible
// escape subset is always valid YAML).
// Pre-reserves worst-case space then writes via raw pointer — same strategy
// as json_escape_string in glazer_json.hpp. ESCAPE_TAB replaces the switch.
// Uses find_escape_pos (from glazer_json.hpp, defined below) which detects
// all control chars < 0x20, '"', and '\' in bulk via NEON/AVX2/SSE2/table.
void emit_double_quoted(std::string_view s) {
m_out.ensure(s.size() * 6 + 2);
char* dst = m_out.m_data + m_out.m_len;
*dst++ = '"';
const char* p = s.data();
const char* end = p + s.size();
while (p < end) {
const char* special = find_escape_pos(p, end);
size_t run = static_cast<size_t>(special - p);
if (run) { memcpy(dst, p, run); dst += run; }
p = special;
if (p >= end) break;
const EscapeEntry& e = ESCAPE_TAB[(unsigned char)*p++];
memcpy(dst, e.seq, e.len);
dst += e.len;
}
*dst++ = '"';
m_out.m_len = static_cast<size_t>(dst - m_out.m_data);
}
// Emit a string scalar, choosing plain / single-quoted / double-quoted.
void emit_string_scalar(std::string_view s) {
if (is_safe_plain_scalar(s)) m_out.push(s);
else if (needs_double_quote(s)) emit_double_quoted(s);
else emit_single_quoted(s);
}
// -------------------------------------------------------------------------
// Top-level entry point
// -------------------------------------------------------------------------
bool encode(ERL_NIF_TERM term) {
if (!encode_node(term, 0)) return false;
m_out.push('\n');
return true;
}
// -------------------------------------------------------------------------
// Node dispatch
//
// `indent` is the column of the container that owns the node currently
// being encoded. Scalars are written inline; collections recurse with
// indent+2 (mappings) or the same indent (sequences nested directly under
// a mapping key, per the same-indent convention the decoder accepts).
// -------------------------------------------------------------------------
bool encode_node(ERL_NIF_TERM term, size_t indent) {
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;
emit_string_scalar({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_FLOAT:
return encode_float(term);
case ERL_NIF_TERM_TYPE_ATOM:
return encode_atom(term);
case ERL_NIF_TERM_TYPE_MAP: {
size_t size;
enif_get_map_size(m_env, term, &size);
if (size == 0) { m_out.push("{}", 2); return true; }
return encode_map(term, indent);
}
case ERL_NIF_TERM_TYPE_LIST:
if (enif_is_empty_list(m_env, term)) { m_out.push("[]", 2); return true; }
return encode_sequence(term, indent);
case ERL_NIF_TERM_TYPE_TUPLE: {
// {[{K,V}...]} proplist -> mapping
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])) [[unlikely]]
return error("tuple is not an object", term);
if (enif_is_empty_list(m_env, tp[0])) {
m_out.push("{}", 2);
return true;
}
return encode_proplist(tp[0], indent);
}
default:
return error("unsupported term type", term);
}
}
bool encode_float(ERL_NIF_TERM term) {
double d;
if (!enif_get_double(m_env, term, &d)) return false;
if (std::isnan(d)) { m_out.push(".nan", 4); return true; }
if (std::isinf(d)) {
if (d < 0) m_out.push("-.inf", 5);
else m_out.push(".inf", 4);
return true;
}
char buf[32];
auto [e, ec] = std::to_chars(buf, buf+32, d, std::chars_format::general);
if (ec == std::errc{}) [[likely]] {
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;
}
bool encode_atom(ERL_NIF_TERM term) {
if (m_opts.null_term && 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; }
if (enif_is_identical(term, AM_INFINITY)) { m_out.push(".inf", 4); return true; }
if (enif_is_identical(term, AM_NEG_INFINITY)) { m_out.push("-.inf", 5); return true; }
if (enif_is_identical(term, AM_NAN)) { m_out.push(".nan", 4); return true; }
std::string_view sv;
if (!atom_to_sv(m_env, term, m_atom_buf, sizeof(m_atom_buf), sv)) return false;
emit_string_scalar(sv);
return true;
}
static void push_indent(OutBuf& out, size_t n) {
static constexpr char SPACES[] = " "; // 32 spaces
while (n >= sizeof(SPACES) - 1) { out.push(SPACES, sizeof(SPACES) - 1); n -= sizeof(SPACES) - 1; }
if (n) out.push(SPACES, n);
}
// Encodes the value of a mapping key at the given indent (the column of
// the key itself). Scalars are written inline after "key: "; collections
// start on the next line(s): nested mappings indent to indent+2, nested
// sequences are placed at the same `indent` (same-indent convention).
bool encode_map_value(ERL_NIF_TERM term, size_t indent) {
switch (enif_term_type(m_env, term)) {
case ERL_NIF_TERM_TYPE_MAP: {
size_t size;
enif_get_map_size(m_env, term, &size);
if (size == 0) { m_out.push(" {}", 3); return true; }
m_out.push('\n');
return encode_map(term, indent + 2);
}
case ERL_NIF_TERM_TYPE_LIST: {
if (enif_is_empty_list(m_env, term)) { m_out.push(" []", 3); return true; }
m_out.push('\n');
return encode_sequence(term, indent);
}
case ERL_NIF_TERM_TYPE_TUPLE: {
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])) [[unlikely]]
return error("tuple is not an object", term);
if (enif_is_empty_list(m_env, tp[0])) {
m_out.push(" {}", 3);
return true;
}
m_out.push('\n');
return encode_proplist(tp[0], indent + 2);
}
default:
m_out.push(' ');
return encode_node(term, indent);
}
}
// -------------------------------------------------------------------------
// Mappings / sequences
// -------------------------------------------------------------------------
// `at_line_start` is false when the first entry continues a line already
// begun by the caller (e.g. right after "- " for a mapping that is a
// sequence item) — in that case the first entry's indent is skipped.
bool encode_map(ERL_NIF_TERM term, size_t indent, bool at_line_start = true) {
auto iter = MapIterator::create(m_env, term);
if (!iter) return false;
ERL_NIF_TERM k, v;
bool first = true;
while (iter->get_pair(&k, &v)) {
if (!first || at_line_start) { if (!first) m_out.push('\n'); push_indent(m_out, indent); }
first = false;
if (!encode_key(k) || !encode_map_value(v, indent)) return false;
iter->next();
}
return true;
}
bool encode_proplist(ERL_NIF_TERM list, size_t indent, bool at_line_start = true) {
ERL_NIF_TERM h, t = list;
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) [[unlikely]]
return error("proplist element is not a 2-tuple", h);
if (!first || at_line_start) { if (!first) m_out.push('\n'); push_indent(m_out, indent); }
first = false;
if (!encode_key(pp[0]) || !encode_map_value(pp[1], indent)) return false;
}
if (!enif_is_empty_list(m_env, t)) [[unlikely]]
return error("cannot encode improper list as proplist", t);
return true;
}
bool encode_key(ERL_NIF_TERM k) {
switch (enif_term_type(m_env, k)) {
case ERL_NIF_TERM_TYPE_BITSTRING: {
ErlNifBinary bin;
if (!enif_inspect_binary(m_env, k, &bin)) return false;
emit_string_scalar({reinterpret_cast<const char*>(bin.data), bin.size});
break;
}
case ERL_NIF_TERM_TYPE_ATOM: {
std::string_view sv;
if (!atom_to_sv(m_env, k, m_atom_buf, sizeof(m_atom_buf), sv)) return false;
emit_string_scalar(sv);
break;
}
case ERL_NIF_TERM_TYPE_INTEGER:
if (!glz::BigInt::encode(m_env, k, m_out)) return false;
break;
default:
return error("unsupported map key type", k);
}
m_out.push(':');
return true;
}
// Sequence items are placed at `indent` (the same column as the mapping
// key that owns the sequence, or 0 at the document root), each starting
// with "- ". Nested mappings under a "- " continue at indent+2.
bool encode_sequence(ERL_NIF_TERM list, size_t indent) {
ERL_NIF_TERM h, t = list;
bool first = true;
while (enif_get_list_cell(m_env, t, &h, &t)) {
if (!first) m_out.push('\n');
first = false;
push_indent(m_out, indent);
m_out.push("- ", 2);
if (!encode_item(h, indent + 2)) return false;
}
if (!enif_is_empty_list(m_env, t)) [[unlikely]]
return error("cannot encode improper list as sequence", t);
return true;
}
// Encodes a sequence item, which sits right after "- " on the dash's line.
bool encode_item(ERL_NIF_TERM term, size_t indent) {
switch (enif_term_type(m_env, term)) {
case ERL_NIF_TERM_TYPE_MAP: {
size_t size;
enif_get_map_size(m_env, term, &size);
if (size == 0) { m_out.push("{}", 2); return true; }
return encode_map(term, indent, false);
}
case ERL_NIF_TERM_TYPE_LIST: {
if (enif_is_empty_list(m_env, term)) { m_out.push("[]", 2); return true; }
// A nested sequence item: "- - ..." (the inner sequence's first
// dash immediately follows this item's "- ").
return encode_sequence_inline(term, indent);
}
case ERL_NIF_TERM_TYPE_TUPLE: {
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])) {
if (enif_is_empty_list(m_env, tp[0])) { m_out.push("{}", 2); return true; }
return encode_proplist(tp[0], indent, false);
}
return false;
}
default:
return encode_node(term, indent);
}
}
// A sequence nested directly inside another sequence's item: the first
// "- " has already been written by the caller; subsequent items continue
// on their own line at `indent` ("- - first\n - second").
bool encode_sequence_inline(ERL_NIF_TERM list, size_t indent) {
ERL_NIF_TERM h, t = list;
bool first = true;
while (enif_get_list_cell(m_env, t, &h, &t)) {
if (!first) { m_out.push('\n'); push_indent(m_out, indent); }
m_out.push("- ", 2);
if (!encode_item(h, indent + 2)) return false;
first = false;
}
if (!enif_is_empty_list(m_env, t)) [[unlikely]]
return error("cannot encode improper list as sequence", t);
return true;
}
private:
template <int N>
bool error(const char (&err)[N], ERL_NIF_TERM term) {
m_err = err;
m_err_term = term;
return false;
}
};
} // namespace glz