C++ port of the canonical TypeScript implementation.
Status: complete. Full TS-canonical parity: all 48 functions, 15 type bit-flags, 3 mode constants (
M_KEYPRE/M_KEYPOST/M_VAL),SKIP/DELETEsentinels (pointer-identity), and theInjectionstate machine.inject/transform/validate/selectall dispatch through the canonical injector machinery: 10 transform commands, 6 validate checkers, 4 select operators.Passes the full shared corpus. Run locally with
make testfromcpp/. Per-file pass counts are written tocorpus-scoreboard.jsonafter each run; the committed baseline lives attest-baseline.json.
For motivation, language-neutral concepts, and the cross-language
parity matrix, see the top-level README and
REPORT.md. For the in-depth guide (tutorial, recipes,
explanation), see DOCS.md.
In the monorepo:
cd cpp
make test # smoke + corpus driver (the default build target)
make smoke # just the smoke test
make corpus # just the corpus driver
make sanitize # build + run with ASan + UBSan
make check_leak # build + run under valgrindThe library is header-only across three files in src/:
value.hpp—Value(astd::variant-based tagged type), the in-treeOrderedMap,Sentinel, type bit-flags, predicates.value_io.hpp— JSON parse/serialise via an in-tree, hand-written recursive-descent parser/printer (no third-party dependency).voxgig_struct.hpp— main API: utilities,getpath/setpath/walk/merge/inject/transform/validate/selectplus alltransform_*/validate_*/select_*injectors.
Namespace voxgig::structlib. Requires C++17 (for std::variant /
structured bindings). The library proper has no third-party
dependency — runtime values use the custom Value type and JSON text is
handled in-tree. nlohmann/json is used
only by the test harness (corpus loading), so make test expects its
header on the include path (e.g. /usr/include/nlohmann/json.hpp).
#include "value_io.hpp" // pulls in value.hpp + voxgig_struct.hpp, plus parse_json
using namespace voxgig::structlib;
int main() {
// Build a value and read a deep path.
Value store = parse_json(R"({"db":{"host":"localhost","port":5432}})");
Value host = getpath_v(store, Value("db.host")); // "localhost"
// Reshape by example.
Value out = transform(
parse_json(R"({"user":{"first":"Ada"},"age":36})"),
parse_json(R"({"name":"`user.first`","years":"`age`"})"));
// {"name":"Ada","years":36}
return 0;
}(Construct Values with parse_json(json_text) (declared in
value_io.hpp) or the typed constructors; see
DOCS.md and src/value.hpp for the full set.)
Functions take const Value& arguments and return Value (or
std::vector<Value> for select). The full, example-by-example reference
is in DOCS.md; the canonical semantics for every function
are in the top-level reference.
Two C++-specific naming points (the names are otherwise the canonical ones):
_v("value-style") suffix onwalk_v,merge_v,getpath_v,setpath_v— disambiguates the public value API from header-internal helpers of the same root name.typename_strinstead oftypename—typenameis a reserved C++ keyword.
The parity check (../tools/check_parity.py) maps these back to the
canonical names, so the port reports full parity.
Build Values with the typed constructors, jm({...}) (map) and
jt({...}) (list), or parse_json(text). The examples below use whichever
is clearest.
bool ismap(const Value& v);
bool islist(const Value& v);
bool iskey(const Value& v);
bool isempty(const Value& v);ismap(jm({"a", 1})); // trueislist(jt({1, 2})); // trueiskey(Value("name")); // trueisempty(jt({})); // trueint typify(const Value& value);
std::string typename_str(int t); // also: typename_str(const Value&)typify(Value(int64_t(1))); // T_scalar | T_number | T_integer (201326720)typename_str(8192); // "map" (8192 == T_map)std::string strkey(const Value& key);
Value getelem(const Value& val, const Value& key, const Value& alt = Value::undef());
Value setprop(Value parent, const Value& key, const Value& val);
Value delprop(Value parent, const Value& key);
bool haskey(const Value& v, const Value& key);
std::vector<Value> items(const Value& v); // [key, val] pairsstrkey(Value(2.2)); // "2"getelem(jt({10, 20, 30}), Value(-1)); // 30setprop(jm({"a", 1}), Value("b"), Value(2)); // {a:1, b:2}delprop(jm({"a", 1, "b", 2}), Value("a")); // {b:2}haskey(jm({"a", 1}), Value("a")); // trueitems(jm({"a", 1, "b", 2})); // {{"a", 1}, {"b", 2}}Value setpath_v(const Value& store, const Value& path, const Value& val);
std::string pathify(const Value& v, int startin = 0, int endin = 0);setpath_v(jm({"a", 1, "b", 2}), Value("b"), Value(22)); // {a:1, b:22}pathify(jt({"a", "b", "c"})); // "a.b.c"Value merge_v(const Value& list, int maxdepth = MAXDEPTH);
Value clone(const Value& v);
Value flatten(const Value& list, int depth = 1);Last input wins; maps deep-merge; lists merge by index:
merge_v(jt({
jm({"a", 1, "b", 2, "k", jt({10, 20}), "x", jm({"y", 5, "z", 6})}),
jm({"b", 3, "d", 4, "e", 8, "k", jt({11}), "x", jm({"y", 7})}),
}));
// {a:1, b:3, d:4, e:8, k:[11, 20], x:{y:7, z:6}}clone(jm({"a", jm({"b", jt({1, 2})})})); // {a:{b:[1,2]}} (a deep copy)flatten(jt({1, jt({2, jt({3})})})); // [1, 2, [3]] (one level by default)std::string escre(const Value& v);
std::string escurl(const Value& v);
std::string join(const Value& arr, const std::string& sep = ",", bool url = false);escre(Value("a.b+c")); // "a\\.b\\+c"escurl(Value("hello world?")); // "hello%20world%3F"join(jt({"a", "b", "c"}), "/"); // "a/b/c"Value inject(const Value& val, const Value& store, Injection* injdef = nullptr);
Value validate(const Value& data, const Value& spec, const Value& options = Value::undef());
std::vector<Value> select(const Value& children, const Value& query);// Backtick refs in strings are replaced by store values.
inject(jm({"x", "`a`", "y", 2}), jm({"a", 1})); // {x:1, y:2}// Validate against a shape (throws on mismatch).
validate(jm({"name", "Ada", "age", 36}),
jm({"name", "`$STRING`", "age", "`$INTEGER`"}));
// {name:"Ada", age:36}// Find children matching a query.
select(jm({"a", jm({"name", "Alice", "age", 30}),
"b", jm({"name", "Bob", "age", 25})}),
jm({"age", 30}));
// [{name:"Alice", age:30, $KEY:"a"}]Runtime values are the in-tree Value type (a std::variant tagged
union) with an in-tree insertion-ordered OrderedMap. Nested maps and
lists are reference-stable — the property the canonical algorithm relies on
for walk/merge/inject/setpath.
value.hpp/value_io.hpp parse and serialise JSON in-tree; the library
links no JSON dependency. nlohmann/json appears only in the test driver.
The port follows the shared Group A/B rule (see
../UNDEF_SPEC.md): readers treat a stored null as
"no value"; value-processors preserve it. Value::undef() is the absent
sentinel used as the default alt.
Uniform six-function regex API (see /design/REGEX_API.md). The C++ port
wraps <regex> (C++11), which defaults to the ECMAScript dialect.
| Function | Maps to |
|---|---|
re_compile(pattern) |
std::regex(pattern) (throws std::regex_error on bad pattern) |
re_test(pattern, input) |
std::regex_search → bool |
re_find(pattern, input) |
first match groups as std::vector<std::string> (empty if no match) |
re_find_all(pattern, input) |
std::vector<std::vector<std::string>> |
re_replace(pattern, input, rep) |
std::regex_replace(input, re, rep) |
re_escape(s) |
escape regex metacharacters |
Patterns must stay inside the RE2 subset documented in /design/REGEX.md.
std::regex defaults to ECMAScript syntax and supports backreferences
and lookaround; using them will not be portable.
- libstdc++
<regex>has the worst-in-class catastrophic backtracking. The discovery panel measures ~1.2 s for^(a+)+$over 22 a's plus!. This is well-known and is the reason many production C++ projects avoid<regex>in favour of RE2 or PCRE2. Stay inside the RE2 subset and avoid nested quantifiers; even then, performance won't match the dedicated engines. - Zero-width
replace.re_replace("a*", "abc", "X")returns"XXbXcX"— the ECMA convention shared by all PCRE/ECMA/.NET/Java/Onigmo engines plus the in-tree Thompson ports. Go (RE2) returns"XbXcX"instead; see/design/REGEX_PATHOLOGICAL.md.
See /design/REGEX_PATHOLOGICAL.md for the cross-port pathological-input panel.
cd cpp
make test # compile + run the corpus driver
make lint # clang-tidy + clang-format checkTests live in tests/; the corpus driver reads the shared
fixtures from ../build/test/.