cheatah
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 once
5// 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 is
8// 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> into
12// 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 are
14// 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.hpp
25#include "pool_builder.hpp" // PoolBuilder (the pooled construction policy the Parser owns)
27namespace 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 none
36 * @test ParsersJsonDom.ToViewReadsBothBackingsAndRejectsNonStrings
37 */
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 string
42 * is copied, not a view), so the result is safe to return, cache, or outlive `text`. For the
43 * 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 rejects
46 * malformed input; parse<false>(...) strips every such check from the binary via `if constexpr` and
47 * 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.ParsesEveryScalarKind
55 */
56template <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.OwningContainersAndDumpRoundTrip
67 */
68[[nodiscard]] std::string dump(const Document& value);
70/**
71 * Serialize a Document by APPENDING to the caller's buffer — stream into a preallocated/reused
72 * std::string rather than allocating a fresh one. This is the efficient path (push_back/append
73 * 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.OwningContainersAndDumpRoundTrip
81 */
82void dump(const Document& value, std::string& out);
84/**
85 * A reusable parser that owns reusable node/member POOLS. Parser::parse() builds a Document whose
86 * arrays/objects are VIEWS (ArrayView/ObjectView) into these pools — zero per-container heap
87 * allocation. Reusing ONE Parser across many parses amortizes the pool allocation/page-faults to
88 * ~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 next
91 * parse() on this Parser, and only while the Parser is alive. (For a self-contained, owning
92 * 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, defaulted
95 * to true. With Validate=true the grammar checks bounds/structure and rejects malformed input
96 * (result JSON null, *ok=false). With Validate=false those checks are guarded by `if constexpr` and
97 * therefore removed from the binary ENTIRELY — there is no runtime flag and no branch, and *ok is
98 * not written at all (an unchecked parse has no validity to report). The unchecked form is for
99 * trusted, known-well-formed input (e.g. our own cache); feeding it malformed input is undefined
100 * behavior. Call it as p.parse<false>(text) / p.parse_owning<false>(text).
101 *
102 * @complexity O(n) in the input length
103 * @alloc the pools, reused across parses (amortized ~0 after warm-up); owned only for escaped
104 * strings
105 * @test ParsersJsonDom.PooledParserYieldsViewsIntoSource
106 */
107class Parser {
108public:
109 /**
110 * Parse @p text into a Document whose arrays/objects are VIEWS (ArrayView/ObjectView) into this
111 * Parser's reused pools — zero per-container allocation, amortized to ~0 across parses. The
112 * 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/structure
114 * 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 when
117 * 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 strings
121 * @test ParsersJsonDom.PooledParserYieldsViewsIntoSource
122 */
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 owned
128 * String<std::string> — strings are copied, not views), fully independent of this Parser and of
129 * @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.OwningParseOutlivesItsSource
137 * @crtest ParsersCompileRun.JsonDomParse
138 */
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-call
144 * 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.PooledParserYieldsViewsIntoSource
150 */
151 [[nodiscard]] std::string_view dump(const Document& value);
153private:
154 // ONE ITERATIVE grammar (this IS "all parsing in one class") — the recursion is unrolled into a
155 // loop over an explicit frame_stack_, so nesting depth costs heap, not C++ call frames (no stack
156 // overflow on adversarially deep input). It is parameterized on (1) a compile-time `bool
157 // Validate` that `if constexpr`-gates every bounds/structure check, and (2) a Builder policy
158 // (PoolBuilder -> ArrayView/ObjectView, or OwningBuilder -> OwnedArray/OwnedObject) driven as a
159 // stack machine (begin/add/finish). Compile-time dispatch; no runtime polymorphism. Defined and
160 // 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; the
165 // grammar only needs to know whether it is an object (so it reads keys) and, for an object, the
166 // 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 parses
174 std::string dump_buf_; // reused serialization buffer for dump()
175};
177} // namespace cheatah::parsers::json