Move parser-only expect state out of json_object

`expect` was only meaningful while the parser was building a container,
and only ever read or written from jsonpull.cpp itself; once parsing
finished it was dead weight on every JSON_ARRAY and JSON_HASH (and
present-but-unused on every primitive too). Move it into the parser's
container stack, alongside the shared_ptr to the container it pertains
to:

    struct json_pull::parse_frame {
        json_object_ptr container;
        json_type       expect;
    };
    std::vector<parse_frame> container_stack;

The base class now only carries data-model state (parent, parser, type).
No external caller depended on `expect`, so no sweep was needed outside
jsonpull.cpp.

This change does not, in itself, shrink any json_object: the 4-byte
`expect` field used to live at offset 20 inside the base, where it was
already being eaten by alignment padding for the 8-byte-aligned first
member of every subclass (std::string, std::vector, double). The win is
in the data model, not the byte count -- the 4-byte hole is still
there, but it is now available for a future subclass whose first member
is small enough to slot into it.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Erica Fischer
2026-05-30 17:49:19 -07:00
co-authored by Cursor
parent 4d9a48c3d4
commit bd90f0b4fe
2 changed files with 46 additions and 39 deletions
+35 -34
View File
@@ -115,35 +115,36 @@ static json_object_ptr fabricate_object(json_pull *jp, json_object *parent, json
return make_object(type, parent, jp); return make_object(type, parent, jp);
} }
static inline json_object *current_container(json_pull *j) { static inline json_pull::parse_frame *current_frame(json_pull *j) {
return j->container_stack.empty() ? nullptr : j->container_stack.back().get(); return j->container_stack.empty() ? nullptr : &j->container_stack.back();
} }
static json_object_ptr add_object(json_pull *j, json_type type) { static json_object_ptr add_object(json_pull *j, json_type type) {
json_object *c = current_container(j); json_pull::parse_frame *f = current_frame(j);
json_object *c = f ? f->container.get() : nullptr;
json_object_ptr o = make_object(type, c, j); json_object_ptr o = make_object(type, c, j);
if (c != nullptr) { if (f != nullptr) {
if (c->type == JSON_ARRAY) { if (c->type == JSON_ARRAY) {
if (c->expect == JSON_ITEM) { if (f->expect == JSON_ITEM) {
c->array().push_back(o); c->array().push_back(o);
c->expect = JSON_COMMA; f->expect = JSON_COMMA;
} else { } else {
j->error = "Expected a comma, not a list item"; j->error = "Expected a comma, not a list item";
return nullptr; return nullptr;
} }
} else if (c->type == JSON_HASH) { } else if (c->type == JSON_HASH) {
if (c->expect == JSON_VALUE) { if (f->expect == JSON_VALUE) {
c->entries().back().value = o; c->entries().back().value = o;
c->expect = JSON_COMMA; f->expect = JSON_COMMA;
} else if (c->expect == JSON_KEY) { } else if (f->expect == JSON_KEY) {
if (type != JSON_STRING) { if (type != JSON_STRING) {
j->error = "Hash key is not a string"; j->error = "Hash key is not a string";
return nullptr; return nullptr;
} }
c->entries().push_back({o, nullptr}); c->entries().push_back({o, nullptr});
c->expect = JSON_COLON; f->expect = JSON_COLON;
} else { } else {
j->error = "Expected a comma or colon"; j->error = "Expected a comma or colon";
return nullptr; return nullptr;
@@ -229,8 +230,7 @@ again:
if (o == nullptr) { if (o == nullptr) {
return nullptr; return nullptr;
} }
o->expect = JSON_ITEM; j->container_stack.push_back({o, JSON_ITEM});
j->container_stack.push_back(o);
if (cb != nullptr) { if (cb != nullptr) {
cb(JSON_ARRAY, j, state); cb(JSON_ARRAY, j, state);
@@ -240,25 +240,26 @@ again:
} }
case ']': { case ']': {
json_object *cc = current_container(j); json_pull::parse_frame *f = current_frame(j);
if (cc == nullptr) { if (f == nullptr) {
j->error = "Found ] at top level"; j->error = "Found ] at top level";
return nullptr; return nullptr;
} }
json_object *cc = f->container.get();
if (cc->type != JSON_ARRAY) { if (cc->type != JSON_ARRAY) {
j->error = "Found ] not in an array"; j->error = "Found ] not in an array";
return nullptr; return nullptr;
} }
if (cc->expect != JSON_COMMA) { if (f->expect != JSON_COMMA) {
if (!(cc->expect == JSON_ITEM && cc->array().size() == 0)) { if (!(f->expect == JSON_ITEM && cc->array().size() == 0)) {
j->error = "Found ] without final element"; j->error = "Found ] without final element";
return nullptr; return nullptr;
} }
} }
json_object_ptr ret = j->container_stack.back(); json_object_ptr ret = f->container;
j->container_stack.pop_back(); j->container_stack.pop_back();
return ret; return ret;
} }
@@ -270,8 +271,7 @@ again:
if (o == nullptr) { if (o == nullptr) {
return nullptr; return nullptr;
} }
o->expect = JSON_KEY; j->container_stack.push_back({o, JSON_KEY});
j->container_stack.push_back(o);
if (cb != nullptr) { if (cb != nullptr) {
cb(JSON_HASH, j, state); cb(JSON_HASH, j, state);
@@ -281,25 +281,26 @@ again:
} }
case '}': { case '}': {
json_object *cc = current_container(j); json_pull::parse_frame *f = current_frame(j);
if (cc == nullptr) { if (f == nullptr) {
j->error = "Found } at top level"; j->error = "Found } at top level";
return nullptr; return nullptr;
} }
json_object *cc = f->container.get();
if (cc->type != JSON_HASH) { if (cc->type != JSON_HASH) {
j->error = "Found } not in a hash"; j->error = "Found } not in a hash";
return nullptr; return nullptr;
} }
if (cc->expect != JSON_COMMA) { if (f->expect != JSON_COMMA) {
if (!(cc->expect == JSON_KEY && cc->entries().size() == 0)) { if (!(f->expect == JSON_KEY && cc->entries().size() == 0)) {
j->error = "Found } without final element"; j->error = "Found } without final element";
return nullptr; return nullptr;
} }
} }
json_object_ptr ret = j->container_stack.back(); json_object_ptr ret = f->container;
j->container_stack.pop_back(); j->container_stack.pop_back();
return ret; return ret;
} }
@@ -366,17 +367,17 @@ again:
/////////////////////////// Comma /////////////////////////// Comma
case ',': { case ',': {
json_object *cc = current_container(j); json_pull::parse_frame *f = current_frame(j);
if (cc != nullptr) { if (f != nullptr) {
if (cc->expect != JSON_COMMA) { if (f->expect != JSON_COMMA) {
j->error = "Found unexpected comma"; j->error = "Found unexpected comma";
return nullptr; return nullptr;
} }
if (cc->type == JSON_HASH) { if (f->container->type == JSON_HASH) {
cc->expect = JSON_KEY; f->expect = JSON_KEY;
} else { } else {
cc->expect = JSON_ITEM; f->expect = JSON_ITEM;
} }
} }
@@ -390,18 +391,18 @@ again:
/////////////////////////// Colon /////////////////////////// Colon
case ':': { case ':': {
json_object *cc = current_container(j); json_pull::parse_frame *f = current_frame(j);
if (cc == nullptr) { if (f == nullptr) {
j->error = "Found colon at top level"; j->error = "Found colon at top level";
return nullptr; return nullptr;
} }
if (cc->expect != JSON_COLON) { if (f->expect != JSON_COLON) {
j->error = "Found unexpected colon"; j->error = "Found unexpected colon";
return nullptr; return nullptr;
} }
cc->expect = JSON_VALUE; f->expect = JSON_VALUE;
if (cb != nullptr) { if (cb != nullptr) {
cb(JSON_COLON, j, state); cb(JSON_COLON, j, state);
+11 -5
View File
@@ -72,7 +72,6 @@ struct json_object {
json_pull *parser = nullptr; json_pull *parser = nullptr;
json_type type; json_type type;
int expect = 0; // used by the parser on JSON_ARRAY / JSON_HASH nodes
json_object(json_type t) : type(t) {} json_object(json_type t) : type(t) {}
json_object(json_type t, json_object *p, json_pull *pl) : parent(p), parser(pl), type(t) {} json_object(json_type t, json_object *p, json_pull *pl) : parent(p), parser(pl), type(t) {}
@@ -190,10 +189,17 @@ struct json_pull {
ssize_t buffer_head = 0; ssize_t buffer_head = 0;
// Stack of currently-open containers; the top is the innermost container // Stack of currently-open containers; the top is the innermost container
// being parsed. Replaces the previous single `container` pointer / parent // being parsed. Each frame also remembers what token is expected next
// walk, which previously required enable_shared_from_this<json_object> // (an item, a comma, a key, a colon, or a value). This stack is the
// on every json_object instance (16 extra bytes per node). // only place the parser-only `expect` state lives, so it does not
std::vector<json_object_ptr> container_stack; // pollute json_object once parsing finishes. Replaces the previous
// single `container` pointer / parent walk, which previously required
// enable_shared_from_this<json_object> on every json_object instance.
struct parse_frame {
json_object_ptr container;
json_type expect;
};
std::vector<parse_frame> container_stack;
json_object_ptr root; json_object_ptr root;
std::string number_buffer; std::string number_buffer;