// core/json — the ONE hand-rolled JSON lexical layer. Pure: standard library only // — NO REAPER, NO SWELL, NO VST3. // // Owns the lexical half of the house JSON dialect: escape-aware string literals // (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), bare scalar tokens, number // parsing (strtod/strtoll, full-token + ERANGE rejection), key+':' consumption, // unknown-value skipping, and the emit side (escaping, %.17g/%d/%lld rendering, // the scoped object writer). Domain grammars — which keys exist, what shape each // value takes — stay in the consumers (bank_model, bank_book, view_mode_model, // origin_ledger, tail_control). // // Byte-compatibility contract (load-bearing): the emit helpers reproduce the prior // per-module writers EXACTLY — writeEscaped's escape set, %.17g for doubles // (shortest form that round-trips every IEEE-754 double bit-for-bit), plain // decimal for ints — so a re-serialized blob is byte-identical to what the // pre-extraction writers produced. Persisted .rpp ext-state must not shift by a byte. #pragma once #include #include #include namespace reasampler::json { // -- emit helpers (writer side) ---------------------------------------------- // Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX // for remaining control chars, everything else verbatim (UTF-8 passes through). void writeEscaped(std::string& out, const std::string& s); // Number rendering. %.17g is the shortest form that round-trips every IEEE-754 // double exactly, so deserialize(serialize(x)) == x holds bit-for-bit. std::string numToStr(double v); std::string numToStr(std::int64_t v); std::string numToStr(int v); // Flat homogeneous arrays: ["a","b"] / [1,2]. Empty vector -> "[]". void writeStringArray(std::string& out, const std::vector& v); void writeIntArray(std::string& out, const std::vector& v); // Scoped object writer: appends '{' on construction and '}' on destruction, with // comma separation handled internally. Nested values are written by keyBegin() // followed by the caller emitting the value (e.g. a nested Writer scope or an // array). NOTE the destructor-close means an enclosing scope must END (brace // block) before the built string is returned — see the NRVO note in the // consumers' serialize() implementations. class Writer { public: explicit Writer(std::string& out) : out_(out) { out_ += '{'; } ~Writer() { out_ += '}'; } Writer(const Writer&) = delete; Writer& operator=(const Writer&) = delete; // "key": — rawValue appended verbatim (numbers, bools, null, // pre-serialized nested blobs). void keyRaw(const char* key, const std::string& rawValue) { sep(); writeEscaped(out_, key); out_ += ':'; out_ += rawValue; } // "key":"value" — value escaped. void keyStr(const char* key, const std::string& value) { sep(); writeEscaped(out_, key); out_ += ':'; writeEscaped(out_, value); } // "key": — caller writes the value immediately after. void keyBegin(const char* key) { sep(); writeEscaped(out_, key); out_ += ':'; } private: void sep() { if (first_) first_ = false; else out_ += ','; } std::string& out_; bool first_ = true; }; // -- Reader — the lexical cursor (parser side) ------------------------------- // // Every method returns false on malformed input and never reads out of bounds. // Only the subset the house writers emit is supported. The reader borrows the // input string — it must outlive the Reader. class Reader { public: explicit Reader(const std::string& s) : s_(s) {} bool eof() const { return pos_ >= s_.size(); } void skipWs(); // Consumes `c` (after whitespace). False without advancing past `c` if the // next non-ws char differs. bool consume(char c); // JSON string literal (escapes + \uXXXX incl. surrogate pairs -> UTF-8). bool parseString(std::string& out); // Bare token (number / true / false / null) up to the next structural char. bool parseRawScalar(std::string& out); // Numbers: full-token parse; trailing bytes or ERANGE reject. parseInt // additionally rejects values outside [INT_MIN, INT_MAX]. bool parseDouble(double& out); bool parseInt64(std::int64_t& out); bool parseInt(int& out); bool parseBool(bool& out); // Peeks for the `null` token; consumes it if present (wasNull=true), // otherwise leaves the position untouched (wasNull=false). Returns false // only on eof. bool expectNullOr(bool& wasNull); // An object member key + ':'. bool parseKey(std::string& key); // Homogeneous arrays. Appends to `out`; empty array is valid. bool parseStringArray(std::vector& out); bool parseIntArray(std::vector& out); // Skips one value of any shape (string / object / array / bare scalar) — // forward-compat for unknown keys. bool skipValue(); // Captures the raw source text of one value verbatim (string-aware brace // matching), so a nested blob can be handed to its own parser — the // bank_book -> BankModel::deserialize seam. bool captureValue(std::string& raw); private: const std::string& s_; std::size_t pos_ = 0; }; } // namespace reasampler::json