This file is for AI coding tools and human contributors working in this repository. It follows the common AGENTS.md shape used by public projects: project overview, commands, architecture, style, testing, security, and agent-specific rules. Keep it practical: prefer facts that can be verified from source, build files, and tests.
If a subdirectory gets its own AGENTS.md later, treat that more specific file
as overriding this root guide for files under that subtree.
sonic-cpp is a header-only, SIMD-accelerated C++ JSON parser/serializer.
The public API is under namespace sonic_json and is normally consumed through:
include/sonic/sonic.hsonic_json::Documentsonic_json::Node
The README says C++11 or above, but the active CMake and Bazel builds use C++17. Treat build files and tests as the practical source of truth.
Primary source directories:
include/sonic/dom/: DOM tree, document, parser, handlers, JSON pointer.include/sonic/internal/: low-level stack, SIMD helpers, arch dispatch.include/sonic/internal/arch/: x86/Arm/SVE2 implementation details.include/sonic/jsonpath/: JSONPath query and dump helpers.include/sonic/experiment/: experimental helpers such as lazy update.tests/: GoogleTest unit tests.benchmark/: benchmark binary sources.fuzz/: CMake-only fuzz target.
Important public plumbing:
- Errors:
include/sonic/error.h - Allocator:
include/sonic/allocator.h - Internal stack:
include/sonic/internal/stack.h - Write buffer:
include/sonic/writebuffer.h - Parse flags:
include/sonic/dom/flags.h
Before changing behavior:
- Read the relevant public header and its tests.
- Search with
rg; avoid broad filesystem scans. - Preserve existing API style unless the task explicitly asks for a breaking API change.
- Add focused tests before or with behavior changes.
- Run the smallest relevant test first, then the broader suite if the change touches shared parsing, DOM, allocator, SIMD, or serialization code.
When reviewing changes:
- Lead with correctness bugs, memory-safety risks, API compatibility problems, and missing tests.
- Pay special attention to allocation failure paths. Silent failure is usually not acceptable in new code.
- Do not simplify arch dispatch or SIMD code without checking both build flags and tests.
Use this checklist before handing work back:
- The change is scoped to the requested behavior.
- Public API compatibility has been considered and documented if affected.
- Allocation failures either propagate
SonicErroror preserve the old object state for legacy APIs. - Relevant unit tests were added or updated.
- The smallest relevant test was run.
- Broader tests were run when shared parser, DOM, allocator, SIMD, or serialization behavior changed.
- No build outputs, dependency caches, or benchmark artifacts were edited.
Quick command recap:
| Task | Command |
|---|---|
| Configure CMake build | cmake -S . -B build |
| Build CMake unit test | cmake --build build --target unittest -j |
| Run CMake unit test | ./build/tests/unittest |
| Run Bazel unit test | bazel run :unittest --//:sonic_arch=haswell --//:sonic_dispatch=static |
| Run full Bazel helper | bash scripts/unittest.sh -g --arch=haswell --dispatch=static |
| Run benchmark with Bazel | bazel run :benchmark --compilation_mode=opt |
Common local flow:
cmake -S . -B build
cmake --build build --target unittest -j
./build/tests/unittestUseful CMake options:
BUILD_UNITTEST=ONby default.BUILD_FUZZ=OFFby default.BUILD_BENCH=OFFby default.ENABLE_SVE2_128=OFFby default.
Sanitizers in CMake tests:
tests/CMakeLists.txtenables ASAN by default throughENABLE_ASAN=ON.- UBSAN can be enabled with
-DENABLE_UBSAN=ON.
Bazel uses Bzlmod. .bazelversion pins the expected version.
Useful commands:
bazel run :unittest --//:sonic_arch=haswell --//:sonic_dispatch=static
bazel run :benchmark --compilation_mode=opt
bash scripts/unittest.sh -g --arch=haswell --dispatch=staticBazel flags:
--//:sonic_arch={default|arm|sve2|westmere|haswell}--//:sonic_dispatch={static|dynamic}--//:sonic_sanitizer={no|gcc|clang}
Note: scripts/unittest.sh accepts --arch=aarch64 and --arch=arm64, then
maps them to Bazel's arm setting. When invoking Bazel directly, use
--//:sonic_arch=arm.
Code in this repository often runs in parser, serializer, allocator, and SIMD hot paths. Allocation failure handling must be explicit enough that callers can distinguish resource failure from valid empty/null JSON values.
Preferred patterns:
- Parser/handler failures should return a concrete
SonicError, especiallykErrorNoMemfor allocation failure. - APIs that can fail should expose failure through the existing project style:
ParseResult,SonicError, boolean success, or allocator error state. - Legacy public APIs may need to keep source-compatible return types. In that case, preserve the previous object state on allocation failure whenever practical and provide or use a checked path for callers that need diagnostics.
- Do not ignore return values from low-level buffer, stack, allocator, parser, or handler methods that can fail.
- Check integer overflow before size arithmetic for allocations, padding, capacity growth, and SIMD lookahead buffers.
Key places to inspect for allocation-sensitive changes:
include/sonic/allocator.hinclude/sonic/internal/stack.hinclude/sonic/writebuffer.hinclude/sonic/dom/parser.hinclude/sonic/dom/handler.hinclude/sonic/dom/schema_handler.hinclude/sonic/dom/dynamicnode.htests/allocator_test.cpp- parser/DOM tests under
tests/
GenericDocument owns or references an allocator and is also the root JSON
node. Re-parsing a document discards the previous tree; any raw pointer,
iterator, or node reference from the old tree must be reacquired after parse.
DNode stores arrays and objects in compact contiguous buffers. Object member
keys are part of the container's lookup invariants; avoid APIs or internal
changes that let callers mutate keys in a way that invalidates cached lookup
structures.
String storage has two modes:
- Non-owning string views for parsed/raw/const strings.
- Allocator-backed copies for APIs that copy strings.
When copying or mutating DOM nodes:
- Prefer commit-after-success updates for operations that can fail halfway.
- Keep source compatibility for existing public APIs where possible.
- Avoid
memmove/raw byte copies for non-trivial node/member objects unless the type is explicitly safe for that operation.
ParseOnDemand is optimized to find a target subtree without fully materializing
the document.
Rule of thumb:
- Default behavior should preserve the fast short-circuit path.
- Full-document validation in on-demand paths is a semantic and performance choice; do not add it by default unless the API or caller explicitly asks for it.
- Skipped branches still must not swallow local parse errors such as malformed strings, invalid numbers, missing separators, or impossible object/array syntax.
Relevant files:
include/sonic/dom/parser.hinclude/sonic/internal/arch/simd_skip.hinclude/sonic/jsonpath/*.htests/document_test.cpptests/jsonpath_test.cpptests/json_tuple_test.cpp
The SIMD layer has static and dynamic dispatch modes. Do not assume only AVX2 exists even though x86 AVX2 is the primary documented target.
Important files:
include/sonic/internal/arch/sonic_cpu_feature.hinclude/sonic/internal/arch/simd_dispatch.hinclude/sonic/internal/arch/avx2/include/sonic/internal/arch/common/include/sonic/internal/arch/neon/include/sonic/internal/arch/sve2-128/include/sonic/internal/arch/x86_ifuncs/
When touching shared SIMD helpers, validate both parser and skip/on-demand
tests. If possible, also test --//:sonic_dispatch=dynamic on x86.
include/sonic/internal/arch/
├── simd_dispatch.h # Central dispatch macro definitions
├── simd_base.h # Base function dispatch (TrailingZeroes, etc.)
├── simd_quote.h # Quote/string function dispatch
├── simd_skip.h # Skip scanner function dispatch
├── simd_itoa.h # Integer-to-string function dispatch
├── simd_str2int.h # String-to-integer function dispatch
├── sonic_cpu_feature.h # CPU feature detection macros
├── common/
│ ├── arm_common/ # Shared ARM implementations
│ │ ├── base.h # Shared bit manipulation (TrailingZeroes, etc.)
│ │ ├── simd.h # Shared to_bitmask() function
│ │ ├── quote.h # Shared Quote() implementation
│ │ ├── skip.inc.h # Shared skip implementations (template-based)
│ │ ├── itoa.h # Shared Utoa_8/Utoa_16 implementations
│ │ └── str2int.h # Shared simd_str2int (scalar fallback)
│ ├── x86_common/ # Shared x86 implementations
│ ├── quote_common.h # Scalar fallback for parseStringInplace/Quote
│ ├── quote_tables.h # Escape lookup tables (kEscapedMap, kQuoteTab*)
│ ├── skip_common.h # Shared EqBytes4, SkipLiteral, IsValidSeparator
│ └── unicode_common.h # Unicode handling (handle_unicode_codepoint, etc.)
├── neon/ # ARM NEON implementation
│ ├── base.h # Re-exports from arm_common
│ ├── simd.h # NEON-specific simd8/simd8x64 templates
│ ├── quote.h # NEON-specific parseStringInplace
│ ├── skip.h # NEON-specific SkipContainer, skip_space
│ ├── itoa.h # Re-exports from arm_common
│ ├── str2int.h # Re-exports from arm_common
│ └── unicode.h # NEON-specific StringBlock, GetNonSpaceBits
├── sve2-128/ # ARM SVE2-128 implementation
│ ├── base.h # Re-exports from arm_common
│ ├── simd.h # SVE2-specific type definitions (svuint8x16_t)
│ ├── quote.h # SVE2-specific parseStringInplace
│ ├── skip.h # SVE2-specific SkipContainer, skip_space
│ ├── itoa.h # Re-exports from arm_common
│ ├── str2int.h # SVE2-specific simd_str2int (optimized)
│ └── unicode.h # SVE2-specific StringBlock, GetNonSpaceBits
├── avx2/ # x86 AVX2 implementation
├── sse/ # x86 SSE implementation
└── x86_ifuncs/ # x86 dynamic dispatch (ifunc)
Each architecture directory (neon/, sve2-128/, avx2/, sse/) must contain these files:
| File | Purpose |
|---|---|
base.h |
Bit manipulation utilities (TrailingZeroes, ClearLowestBit, LeadingZeroes, CountOnes, PrefixXor, Xmemcpy, InlinedMemcmp, InlinedMemcmpEq) |
simd.h |
SIMD vector type definitions (simd8<T>, simd8x64<T>) with operator overloads, to_bitmask(), eq(), lteq() |
quote.h |
parseStringInplace<ParseFlags>() and Quote<SerializeFlags>() functions |
skip.h |
SkipContainer(), skip_space(), skip_space_safe(), plus includes skip.inc.h for GetNextToken(), SkipString(), GetStringBits() |
itoa.h |
Utoa_8() and Utoa_16() for integer-to-string conversion |
str2int.h |
simd_str2int() for string-to-integer conversion |
unicode.h |
StringBlock struct with Find(), HasQuoteFirst(), HasBackslash(), HasUnescaped(), GetNonSpaceBits() |
The entire dispatch mechanism is controlled by two key macros defined in simd_dispatch.h:21-55:
1. CPU Feature Detection (sonic_cpu_feature.h:17-47)
// x86 detection
#if defined(__SSE2__)
#define SONIC_HAVE_SSE
// ... SSE3, SSSE3, SSE4_1, SSE4_2, AVX, AVX2
#endif
// ARM detection
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
#define SONIC_HAVE_NEON
#endif
#if defined(__ARM_FEATURE_SVE2) && (__ARM_FEATURE_SVE_BITS == 128)
#define SONIC_HAVE_SVE2_128
#endif2. Static Dispatch (simd_dispatch.h:25-42)
#if defined(SONIC_STATIC_DISPATCH)
// x86 priority: AVX2 > SSE
#if defined(SONIC_HAVE_AVX2)
#define SONIC_USING_ARCH_FUNC(func) using avx2::func
#define INCLUDE_ARCH_FILE(file) SONIC_STRINGIFY(avx2/file)
#elif defined(SONIC_HAVE_SSE)
#define SONIC_USING_ARCH_FUNC(func) using sse::func
#define INCLUDE_ARCH_FILE(file) SONIC_STRINGIFY(sse/file)
#endif
// ARM priority: SVE2-128 > NEON
#if defined(SONIC_HAVE_SVE2_128)
#define SONIC_USING_ARCH_FUNC(func) using sve2_128::func
#define INCLUDE_ARCH_FILE(file) SONIC_STRINGIFY(sve2-128/file)
#elif defined(SONIC_HAVE_NEON)
#define SONIC_USING_ARCH_FUNC(func) using neon::func
#define INCLUDE_ARCH_FILE(file) SONIC_STRINGIFY(neon/file)
#endif3. Dynamic Dispatch (simd_dispatch.h:44-53)
#elif defined(SONIC_DYNAMIC_DISPATCH)
#if defined(__x86_64__)
#define SONIC_USING_ARCH_FUNC(func) // Empty - uses ifunc resolution
#define INCLUDE_ARCH_FILE(file) SONIC_STRINGIFY(x86_ifuncs/file)
#elif defined(SONIC_HAVE_NEON)
#define SONIC_USING_ARCH_FUNC(func) using neon::func
#define INCLUDE_ARCH_FILE(file) SONIC_STRINGIFY(neon/file)
#endif4. Dispatch Header Pattern (e.g., simd_base.h:17-36)
#include "simd_dispatch.h"
#include INCLUDE_ARCH_FILE(base.h) // Includes e.g., "neon/base.h"
namespace sonic_json {
namespace internal {
SONIC_USING_ARCH_FUNC(TrailingZeroes); // Becomes: using neon::TrailingZeroes;
SONIC_USING_ARCH_FUNC(ClearLowestBit);
// ... more functions
} // namespace internal
} // namespace sonic_jsonNamespace Hierarchy:
sonic_json::internal::
├── common:: # Scalar fallbacks and cross-arch utilities
├── arm_common:: # Shared ARM implementations
├── x86_common:: # Shared x86 implementations
├── neon:: # NEON-specific implementations
├── sve2_128:: # SVE2-128-specific implementations
├── avx2:: # AVX2-specific implementations
├── sse:: # SSE-specific implementations
└── (direct) # Dispatched functions via using declarations
Key Function Patterns:
-
SIMD Vector Types (
simd.h):simd8<T>- 16-byte vector (16 elements of type T)simd8x64<T>- 64-byte block (4 x simd8 chunks)- Methods:
load(),store(),to_bitmask(),eq(),lteq(),reduce_or(), operator overloads
-
String Block Pattern (
unicode.h):struct StringBlock { static StringBlock Find(const uint8_t* src); // Load and analyze 16 bytes template<ParseFlags> bool HasQuoteFirst() const; bool HasBackslash() const; bool HasUnescaped() const; int QuoteIndex() const; int BsIndex() const; uint64_t bs_bits, quote_bits, unescaped_bits; // NEON: bitmasks // OR for SVE2: unsigned bs_index, quote_index, unescaped_index; };
-
String Parsing (
quote.h):template<ParseFlags parseFlags = kParseDefault> sonic_force_inline size_t parseStringInplace(uint8_t*& src, SonicError& err);
-
Skip Functions (
skip.h):sonic_force_inline bool SkipContainer(const uint8_t* data, size_t& pos, size_t len, uint8_t left, uint8_t right); sonic_force_inline uint8_t skip_space(const uint8_t* data, size_t& pos, size_t&, uint64_t&);
Compile-time detection only (no runtime CPUID for ARM):
sonic_cpu_feature.huses compiler predefined macros- ARM:
__ARM_NEON,__ARM_FEATURE_SVE2,__ARM_FEATURE_SVE_BITS - x86:
__SSE2__,__AVX2__, etc. - No runtime feature detection for ARM (static dispatch only)
- x86 supports dynamic dispatch via
x86_ifuncs/using GNU ifunc attribute
Build system integration:
- CMake:
-DENABLE_SVE2_128=ONenables SVE2 compiler flags - Bazel:
--//:sonic_arch={arm|sve2|haswell|westmere} - Bazel:
--//:sonic_dispatch={static|dynamic}
Three levels of code sharing:
-
common/*.h- Architecture-independent:quote_common.h: Scalar fallbackparseStringInplace()andQuote()for x86 dynamic dispatchskip_common.h:EqBytes4(),SkipLiteral(),IsValidSeparator()- pure scalarunicode_common.h:handle_unicode_codepoint(),codepoint_to_utf8(),hex_to_u32_nocheck(),GetEscaped()quote_tables.h:kEscapedMap[256],kQuoteTabLowerCase[256],kQuoteTabUpperCase[256],kNeedEscaped[256]
-
common/arm_common/*.h- ARM-specific shared code:base.h:TrailingZeroes()→__builtin_ctzll(),ClearLowestBit(),LeadingZeroes()→__builtin_clzll(),CountOnes()→__builtin_popcountll(),PrefixXor(),Xmemcpy(),InlinedMemcmp(),InlinedMemcmpEq()simd.h:to_bitmask(uint8x16_t)using NEONvshrn_n_u16quote.h:Quote<SerializeFlags>()using NEON intrinsics, shared by both NEON and SVE2-128skip.inc.h: Template implementations ofGetStringBits<T>(),GetNextToken<N>(),SkipString(),skip_container<T>()- included with#include(notusing)itoa.h:UtoaNeon(),Utoa_8(),Utoa_16()using NEON intrinsicsstr2int.h: Scalar fallbacksimd_str2int()(used by NEON, overridden by SVE2-128)
-
Architecture-specific files (
neon/,sve2-128/):- Thin wrapper headers that
usingdeclarations fromarm_common - Architecture-specific optimizations where needed:
- NEON: Full
simd8<T>andsimd8x64<T>template implementations - SVE2-128: Minimal
simd.h(reuses NEON simd types via../neon/simd.hin skip.h), optimizedsimd_str2int()using SVE2svdot, predicate-basedStringBlockusingsvmatch/svbrkb/svcntp
- NEON: Full
- Thin wrapper headers that
1. Re-export Pattern (most files):
// neon/base.h
#include "../common/arm_common/base.h"
namespace neon {
using arm_common::TrailingZeroes;
using arm_common::ClearLowestBit;
// ...
}2. Template Include Pattern (skip.inc.h):
// neon/skip.h
#include "../common/arm_common/skip.inc.h" // Injects template functions directly
sonic_force_inline bool SkipContainer(...) {
return skip_container<simd8x64<uint8_t>>(data, pos, len, left, right);
}3. Cross-architecture Reuse (SVE2-128 using NEON):
// sve2-128/skip.h
#include "../neon/simd.h" // Reuse NEON simd types for skip_container
sonic_force_inline bool SkipContainer(...) {
// Use NEON implementation since it's faster for comparisons
return skip_container<neon::simd8x64<uint8_t>>(data, pos, len, left, right);
}4. Override Pattern (SVE2-128 str2int):
// sve2-128/str2int.h - does NOT include arm_common/str2int.h
// Instead provides optimized SVE2 implementation
sonic_force_inline uint64_t simd_str2int(const char* c, int& man_nd) {
// Uses svld1, svmatch, svdot, etc.
}| Aspect | NEON | SVE2-128 |
|---|---|---|
| Vector type | uint8x16_t |
svuint8x16_t (fixed 128-bit SVE) |
| Load | vld1q_u8(ptr) |
svld1_u8(svptrue_b8(), ptr) |
| Store | vst1q_u8(ptr, v) |
svst1_u8(svptrue_b8(), ptr, v) |
| Compare | vceqq_u8(v, dup) → bitmask via to_bitmask() |
svmatch(ptrue, v, dup) → predicate |
| Bitmask format | uint64_t (4 bits per lane) |
svbool_t (predicate) |
| Find first | TrailingZeroes(bitmask) >> 2 |
svcntp_b8(ptrue, svbrkb_z(ptrue, pmatch)) |
| StringBlock | Stores 3x uint64_t bitmasks |
Stores 3x unsigned indices (0-15) |
| str2int | Scalar loop from arm_common | Optimized SVE2 svdot implementation |
| SkipContainer | Uses neon::simd8x64 |
Also uses neon::simd8x64 (faster for comparisons) |
| Bazel Flag | CMake Equivalent | Effect |
|---|---|---|
--//:sonic_arch=arm |
(default on ARM) | Enables NEON |
--//:sonic_arch=sve2 |
-DENABLE_SVE2_128=ON |
Enables SVE2-128 (higher priority than NEON) |
--//:sonic_arch=haswell |
(default on x86) | Enables AVX2 |
--//:sonic_arch=westmere |
(manual) | Enables SSE4.2 |
--//:sonic_dispatch=static |
(default) | Static dispatch via macros |
--//:sonic_dispatch=dynamic |
(manual) | Dynamic dispatch via ifunc (x86 only) |
- Follow root
.clang-format(BasedOnStyle: Google). - Keep public headers self-contained.
- Do not add heavyweight dependencies to the header-only library.
- Prefer simple, explicit control flow in parser and allocator code.
- Keep comments sparse and useful; explain invariants and failure handling, not obvious assignments.
- Default to ASCII in source and docs unless a file already uses non-ASCII.
Choose tests based on the touched area:
- Allocator/stack/write buffer:
tests/allocator_test.cpp,tests/writebuffer_test.cpp,tests/parser_oom_test.cpp. - DOM mutation/copy/member map:
tests/node_test.cpp,tests/document_test.cpp,tests/parser_oom_test.cpp. - Full parser and lazy parser:
tests/parser_oom_test.cpp,tests/document_test.cpp. - Parse schema:
tests/parse_schema_test.cpp. - Parse on demand / JSON pointer:
tests/document_test.cpp,tests/json_pointer_test.cpp. - JSONPath / tuple extraction:
tests/jsonpath_test.cpp,tests/json_tuple_test.cpp. - SIMD skip scanner:
tests/skip_test.cpp.
For broad validation, run:
cmake --build build --target unittest -j
./build/tests/unittestor:
bash scripts/unittest.sh -g --arch=haswell --dispatch=staticTesting philosophy:
- Test public behavior first; avoid overfitting tests to private helper details.
- For bug fixes, add a regression test that fails on the old behavior.
- For allocation-failure fixes, assert both the reported error and the preserved state when preservation is part of the contract.
- For parser fixes, include malformed input around the exact branch being changed, not only a happy-path JSON sample.
- For performance-sensitive parser/SIMD changes, keep tests deterministic and put speed measurements in benchmarks, not unit tests.
Use benchmarks for changes that affect:
- object lookup or member insertion/removal,
- parser hot loops,
- SIMD skip/string paths,
- serialization,
- allocator growth behavior.
Commands:
cmake -S . -B build-bench -DBUILD_BENCH=ON
cmake --build build-bench --target bench -j
./build-bench/benchmark/benchor:
bazel run :benchmark --compilation_mode=opt- Do not treat
operator[]missing-member behavior as mutable storage; preferFindMember. - Do not mutate object member keys through iterators or internal aliases unless every affected lookup structure is rebuilt or updated.
- Do not add parse-on-demand full validation by default unless the task accepts a performance/semantic change.
- Do not ignore trailing characters in full parse paths.
- Do not use throwing allocation in low-level parser/SAX paths when the surrounding code expects explicit error propagation.
- Do not change public type layout casually; this is a header-only library and downstream code may depend on source-level details.
- Do not modify generated build outputs,
build/,bazel-*, or benchmark result artifacts unless explicitly asked.
When summarizing work for another tool or reviewer:
- List behavior changes first, then files touched.
- State which tests were run and which were not run.
- Call out API compatibility, memory-safety, and performance tradeoffs.
- Mention any remaining risk if only a narrow test was run.
- For large parser or SIMD changes, include the exact architecture/dispatch mode used for validation.
Inputs should be treated as untrusted JSON. The usage docs state that UTF-8 is assumed and not verified by default. Always check parse results with:
HasParseError()GetParseError()GetErrorOffset()ErrorMsg(...)
Security issues should not be disclosed through public issues. Follow
CONTRIBUTING.md for the reporting contact.