schemagen: support objects with more than 32 properties

Replace the 32-bit `uint32_t seen` bitmask with a `std::vector<uint64_t>`
bitset sized to the actual field count.  `requiredMask` is now a vector of
the same word count, and required-field checks use per-word masking so that
optional fields do not cause false rejections.

Adds big.schema.json + test_big.cpp regression tests covering the issue
reproducer (40 required properties, missing/duplicate at index 32), plus
run_tests.sh to exercise both test_gen.cpp and test_big.cpp.

Fixes #3
This commit is contained in:
2026-06-18 16:31:26 -04:00
parent 7c1c18fe6f
commit ca474e3f99
6 changed files with 400 additions and 19 deletions
+51 -19
View File
@@ -690,19 +690,35 @@ namespace {ns} {{"""
lines.append(" }")
return "\n".join(lines)
def _bitmask(self, obj, pred):
mask = 0
for i, fld in enumerate(obj.fields):
if pred(fld):
mask |= 1 << i
return mask
def _fieldcount(self):
lines = [" int fieldCount(Kind k) const {", " switch (k) {"]
for name, obj in self.b.objects.items():
lines.append(f" case Kind::{name}: return {len(obj.fields)};")
lines.append(" default: return 0;")
lines.append(" }")
lines.append(" }")
return "\n".join(lines)
def _reqmask(self):
lines = [" uint32_t requiredMask(Kind k) const {", " switch (k) {"]
lines = [
" const std::vector<uint64_t> &requiredMask(Kind k) const {",
" switch (k) {",
]
for name, obj in self.b.objects.items():
m = self._bitmask(obj, lambda f: f.required)
lines.append(f" case Kind::{name}: return {hex(m)}u;")
lines.append(" default: return 0u;")
words = (len(obj.fields) + 63) // 64
req = [0] * words
for i, fld in enumerate(obj.fields):
if fld.required:
req[i >> 6] |= 1 << (i & 63)
init = ", ".join(f"{hex(w)}u" for w in req) if words else ""
lines.append(f" case Kind::{name}: {{")
lines.append(f" static const std::vector<uint64_t> m = {{ {init} }};")
lines.append(" return m;")
lines.append(" }")
lines.append(" default: {")
lines.append(" static const std::vector<uint64_t> empty;")
lines.append(" return empty;")
lines.append(" }")
lines.append(" }")
lines.append(" }")
return "\n".join(lines)
@@ -783,6 +799,11 @@ namespace {ns} {{"""
if root_kind is not None and root_cat == event_cat:
lines.append(" if (stack_.empty()) {")
lines.append(f" stack_.push_back(Frame{{{root_kind}, &result_}});")
if event_cat == "Obj":
lines.append(" {")
lines.append(f" int n = fieldCount({root_kind});")
lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);")
lines.append(" }")
lines.append(" return;")
lines.append(" }")
else:
@@ -798,6 +819,10 @@ namespace {ns} {{"""
lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}")
lines.append(" void *p = engage(f, true);")
lines.append(" stack_.push_back(Frame{si.child, p});")
lines.append(" if (isObjectKind(si.child)) {")
lines.append(" int n = fieldCount(si.child);")
lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);")
lines.append(" }")
return "\n".join(lines)
def _builder(self, order):
@@ -849,8 +874,8 @@ private:
struct Frame {{
Kind kind;
void *dest;
int field = -1; // object: selected field (-1 want key, -2 skip)
uint32_t seen = 0; // bitmask of populated fields
int field = -1; // object: selected field (-1 want key, -2 skip)
std::vector<uint64_t> seen; // populated field bitset (object frames)
}};
static constexpr int kWantKey = -1;
static constexpr int kSkip = -2;
@@ -885,7 +910,7 @@ private:
scratch_.clear();
{(" if (p.kind == Kind::RootScalar) { stack_.pop_back(); done_ = true; return; }" if root_kind is None else "")}
if (isObjectKind(p.kind)) {{
if (p.field >= 0) p.seen |= (1u << p.field);
if (p.field >= 0) p.seen[p.field >> 6] |= (1ull << (p.field & 63));
p.field = kWantKey;
}}
}}
@@ -895,16 +920,19 @@ private:
}}
void cbEndObject() {{
if (error_) return;
Frame f = stack_.back();
Frame &f = stack_.back();
if (f.kind == Kind::Skip) {{
stack_.pop_back();
if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete();
return;
}}
if (isObjectKind(f.kind) &&
(f.seen & requiredMask(f.kind)) != requiredMask(f.kind)) {{
reject();
return;
if (isObjectKind(f.kind)) {{
const auto &req = requiredMask(f.kind);
bool missing = false;
for (size_t i = 0; i < req.size(); ++i) {{
if ((f.seen[i] & req[i]) != req[i]) {{ missing = true; break; }}
}}
if (missing) {{ reject(); return; }}
}}
stack_.pop_back();
valueComplete();
@@ -937,7 +965,9 @@ private:
f.field = kSkip;
return;
}}
if (f.seen & (1u << idx)) {{ reject(); return; }} // duplicate key
if (f.seen[idx >> 6] & (1ull << (idx & 63))) {{
reject(); return; // duplicate key
}}
f.field = idx;
}}
@@ -1062,6 +1092,8 @@ private:
{self._matchkey()}
{self._fieldcount()}
{self._reqmask()}
{self._is_object_kind()}