Source
stdlib/parsers/json/json.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
// cheatah::parsers::json — a from-scratch JSON parser (pure C++, no deps).6
//7
// This header declares the public API; the implementation lives in json.cpp. The value model is8
// Node (json/node.hpp) — a class wrapping a std::variant; access its alternatives via .variant().9
//10
// Two parse paths trade copying for lifetime (chosen by the Builder, no runtime polymorphism):11
// * Parser::parse (pooled) is ZERO-COPY — an unescaped string is a String<std::string_view> into12
// the SOURCE text, so `text` (and the Parser's pools) must outlive the Document.13
// * the free parse() / Parser::parse_owning produce a SELF-CONTAINED Document — strings are14
// copied into owned String<std::string> — safe to return, cache, or outlive `text`.15
// An escaped string is always decoded into owned storage. No runtime polymorphism anywhere.17
#include <cstddef>18
#include <span>19
#include <string>20
#include <string_view>21
#include <vector>23
#include "cursor.hpp" // Cursor (used by Parser's private parse methods)24
#include "document.hpp" // Document (= Node) + node.hpp25
#include "pool_builder.hpp" // PoolBuilder (the pooled construction policy the Parser owns)27
namespace cheatah::parsers::json {29
/**30
* Read a Node's characters when it is a string (either backing), else an empty view.31
*32
* @param value the node to read.33
* @return the characters when @p value is a string (either backing), else an empty view.34
* @complexity O(1).35
* @alloc none36
* @test ParsersJsonDom.ToViewReadsBothBackingsAndRejectsNonStrings37
*/38
[[nodiscard]] std::string_view to_view(const Node& value) noexcept;40
/**41
* Parse `text` into a SELF-CONTAINED Document (owning containers AND owned strings — every string42
* is copied, not a view), so the result is safe to return, cache, or outlive `text`. For the43
* zero-copy, source-viewing form (no string copies), reuse a Parser and call Parser::parse.44
*45
* Validate is a COMPILE-TIME switch: the default (true) checks bounds/structure and rejects46
* malformed input; parse<false>(...) strips every such check from the binary via `if constexpr` and47
* does not write *ok — trusted, known-well-formed input only; malformed input is undefined behavior.48
*49
* @param text the JSON source to parse.50
* @param ok if non-null, set true on success and false on a parse error (written only when Validate is true).51
* @return the self-contained Document (JSON null on error when validating).52
* @complexity O(n) in the input length.53
* @alloc allocates the owned document tree (arrays, objects, and copied strings)54
* @test ParsersJsonDom.ParsesEveryScalarKind55
*/56
template <bool Validate = true>57
[[nodiscard]] Document parse(std::string_view text, bool* ok = nullptr);59
/**60
* Serialize a Document to compact JSON text (string contents are re-escaped).61
*62
* @param value the document to serialize.63
* @return the compact JSON text.64
* @complexity O(output size).65
* @alloc allocates the result string and the O(depth) frame stack.66
* @test ParsersJsonDom.OwningContainersAndDumpRoundTrip67
*/68
[[nodiscard]] std::string dump(const Document& value);70
/**71
* Serialize a Document by APPENDING to the caller's buffer — stream into a preallocated/reused72
* std::string rather than allocating a fresh one. This is the efficient path (push_back/append73
* into one growing buffer); it deliberately avoids std::stringstream, which adds formatting,74
* locale, and virtual-streambuf overhead per write. Reserve `out` once and reuse it across calls.75
*76
* @param value the document to serialize.77
* @param out the buffer the JSON text is appended to.78
* @complexity O(output size).79
* @alloc allocates the O(depth) frame stack; grows @p out only past its capacity.80
* @test ParsersJsonDom.OwningContainersAndDumpRoundTrip81
*/82
void dump(const Document& value, std::string& out);84
/**85
* A reusable parser that owns reusable node/member POOLS. Parser::parse() builds a Document whose86
* arrays/objects are VIEWS (ArrayView/ObjectView) into these pools — zero per-container heap87
* allocation. Reusing ONE Parser across many parses amortizes the pool allocation/page-faults to88
* ~0 after warm-up (the reusable-parser model), which is the whole point of option B.89
*90
* LIFETIME: the returned Document VIEWS this Parser's pools, so it is valid only until the next91
* parse() on this Parser, and only while the Parser is alive. (For a self-contained, owning92
* Document — e.g. for the cache — use the free parse() above instead.) No runtime polymorphism.93
*94
* VALIDATION: every parse method takes a compile-time `bool Validate` template parameter, defaulted95
* to true. With Validate=true the grammar checks bounds/structure and rejects malformed input96
* (result JSON null, *ok=false). With Validate=false those checks are guarded by `if constexpr` and97
* therefore removed from the binary ENTIRELY — there is no runtime flag and no branch, and *ok is98
* not written at all (an unchecked parse has no validity to report). The unchecked form is for99
* trusted, known-well-formed input (e.g. our own cache); feeding it malformed input is undefined100
* behavior. Call it as p.parse<false>(text) / p.parse_owning<false>(text).101
*102
* @complexity O(n) in the input length103
* @alloc the pools, reused across parses (amortized ~0 after warm-up); owned only for escaped104
* strings105
* @test ParsersJsonDom.PooledParserYieldsViewsIntoSource106
*/107
class Parser {108
public:109
/**110
* Parse @p text into a Document whose arrays/objects are VIEWS (ArrayView/ObjectView) into this111
* Parser's reused pools — zero per-container allocation, amortized to ~0 across parses. The112
* result is valid only until the next parse or dump() on this Parser, and while the Parser lives.113
* @tparam Validate when true (default) reject malformed input; when false all bounds/structure114
* checks are compiled out (trusted, known-well-formed input only — see the class doc).115
* @param text the JSON source to parse.116
* @param ok if non-null, set to true on success and false on a parse error (only written when117
* Validate is true).118
* @return the parsed Document (JSON null on error when validating).119
* @complexity O(|text|)120
* @alloc none after warm-up (reused pools); owned only for escaped strings121
* @test ParsersJsonDom.PooledParserYieldsViewsIntoSource122
*/123
template <bool Validate = true>124
[[nodiscard]] Document parse(std::string_view text, bool* ok = nullptr);126
/**127
* Parse @p text into a self-contained OWNING Document (OwnedArray/OwnedObject AND owned128
* String<std::string> — strings are copied, not views), fully independent of this Parser and of129
* @p text once returned. This is what the free parse() and the cache use.130
* @tparam Validate as for parse().131
* @param text the JSON source to parse.132
* @param ok if non-null, set to true on success and false on a parse error (Validate=true only).133
* @return a self-contained parsed Document (JSON null on error when validating).134
* @complexity O(|text|)135
* @alloc allocates the owned document tree (arrays, objects, and copied strings)136
* @test ParsersJsonDom.OwningParseOutlivesItsSource137
* @crtest ParsersCompileRun.JsonDomParse138
*/139
template <bool Validate = true>140
[[nodiscard]] Document parse_owning(std::string_view text, bool* ok = nullptr);142
/**143
* Serialize @p value into the Parser's own REUSED buffer and return a view of it — no per-call144
* allocation after warm-up. The view is valid until the next dump() on this Parser.145
* @param value the document to serialize.146
* @return a std::string_view of the serialized JSON (valid until the next dump()).147
* @complexity O(output size)148
* @alloc the O(depth) frame stack per call; the output buffer is reused after warm-up.149
* @test ParsersJsonDom.PooledParserYieldsViewsIntoSource150
*/151
[[nodiscard]] std::string_view dump(const Document& value);153
private:154
// ONE ITERATIVE grammar (this IS "all parsing in one class") — the recursion is unrolled into a155
// loop over an explicit frame_stack_, so nesting depth costs heap, not C++ call frames (no stack156
// overflow on adversarially deep input). It is parameterized on (1) a compile-time `bool157
// Validate` that `if constexpr`-gates every bounds/structure check, and (2) a Builder policy158
// (PoolBuilder -> ArrayView/ObjectView, or OwningBuilder -> OwnedArray/OwnedObject) driven as a159
// stack machine (begin/add/finish). Compile-time dispatch; no runtime polymorphism. Defined and160
// instantiated for both validation modes and both builders in json.cpp.161
template <bool Validate, class Builder>162
bool parse_value(Cursor& c, Node& out, Builder& b);164
// One open container on the parse stack. The Builder owns the partial container itself; the165
// grammar only needs to know whether it is an object (so it reads keys) and, for an object, the166
// pending key awaiting its value.167
struct Frame {168
bool is_object;169
Node key; // the in-progress object key (unused for arrays)170
};172
PoolBuilder pool_; // pooled construction policy, reused across parse() (view path)173
std::vector<Frame> frame_stack_; // explicit recursion stack, reused across parses174
std::string dump_buf_; // reused serialization buffer for dump()175
};177
} // namespace cheatah::parsers::json