From bd90f0b4fe428c1c75da185d727f4a85a538ec51 Mon Sep 17 00:00:00 2001 From: Erica Fischer Date: Sat, 30 May 2026 10:16:56 -0700 Subject: [PATCH] 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 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 --- jsonpull/jsonpull.cpp | 69 ++++++++++++++++++++++--------------------- jsonpull/jsonpull.h | 16 ++++++---- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/jsonpull/jsonpull.cpp b/jsonpull/jsonpull.cpp index 53704ae3..0f2d99fe 100644 --- a/jsonpull/jsonpull.cpp +++ b/jsonpull/jsonpull.cpp @@ -115,35 +115,36 @@ static json_object_ptr fabricate_object(json_pull *jp, json_object *parent, json return make_object(type, parent, jp); } -static inline json_object *current_container(json_pull *j) { - return j->container_stack.empty() ? nullptr : j->container_stack.back().get(); +static inline json_pull::parse_frame *current_frame(json_pull *j) { + return j->container_stack.empty() ? nullptr : &j->container_stack.back(); } 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); - if (c != nullptr) { + if (f != nullptr) { if (c->type == JSON_ARRAY) { - if (c->expect == JSON_ITEM) { + if (f->expect == JSON_ITEM) { c->array().push_back(o); - c->expect = JSON_COMMA; + f->expect = JSON_COMMA; } else { j->error = "Expected a comma, not a list item"; return nullptr; } } else if (c->type == JSON_HASH) { - if (c->expect == JSON_VALUE) { + if (f->expect == JSON_VALUE) { c->entries().back().value = o; - c->expect = JSON_COMMA; - } else if (c->expect == JSON_KEY) { + f->expect = JSON_COMMA; + } else if (f->expect == JSON_KEY) { if (type != JSON_STRING) { j->error = "Hash key is not a string"; return nullptr; } c->entries().push_back({o, nullptr}); - c->expect = JSON_COLON; + f->expect = JSON_COLON; } else { j->error = "Expected a comma or colon"; return nullptr; @@ -229,8 +230,7 @@ again: if (o == nullptr) { return nullptr; } - o->expect = JSON_ITEM; - j->container_stack.push_back(o); + j->container_stack.push_back({o, JSON_ITEM}); if (cb != nullptr) { cb(JSON_ARRAY, j, state); @@ -240,25 +240,26 @@ again: } case ']': { - json_object *cc = current_container(j); - if (cc == nullptr) { + json_pull::parse_frame *f = current_frame(j); + if (f == nullptr) { j->error = "Found ] at top level"; return nullptr; } + json_object *cc = f->container.get(); if (cc->type != JSON_ARRAY) { j->error = "Found ] not in an array"; return nullptr; } - if (cc->expect != JSON_COMMA) { - if (!(cc->expect == JSON_ITEM && cc->array().size() == 0)) { + if (f->expect != JSON_COMMA) { + if (!(f->expect == JSON_ITEM && cc->array().size() == 0)) { j->error = "Found ] without final element"; return nullptr; } } - json_object_ptr ret = j->container_stack.back(); + json_object_ptr ret = f->container; j->container_stack.pop_back(); return ret; } @@ -270,8 +271,7 @@ again: if (o == nullptr) { return nullptr; } - o->expect = JSON_KEY; - j->container_stack.push_back(o); + j->container_stack.push_back({o, JSON_KEY}); if (cb != nullptr) { cb(JSON_HASH, j, state); @@ -281,25 +281,26 @@ again: } case '}': { - json_object *cc = current_container(j); - if (cc == nullptr) { + json_pull::parse_frame *f = current_frame(j); + if (f == nullptr) { j->error = "Found } at top level"; return nullptr; } + json_object *cc = f->container.get(); if (cc->type != JSON_HASH) { j->error = "Found } not in a hash"; return nullptr; } - if (cc->expect != JSON_COMMA) { - if (!(cc->expect == JSON_KEY && cc->entries().size() == 0)) { + if (f->expect != JSON_COMMA) { + if (!(f->expect == JSON_KEY && cc->entries().size() == 0)) { j->error = "Found } without final element"; return nullptr; } } - json_object_ptr ret = j->container_stack.back(); + json_object_ptr ret = f->container; j->container_stack.pop_back(); return ret; } @@ -366,17 +367,17 @@ again: /////////////////////////// Comma case ',': { - json_object *cc = current_container(j); - if (cc != nullptr) { - if (cc->expect != JSON_COMMA) { + json_pull::parse_frame *f = current_frame(j); + if (f != nullptr) { + if (f->expect != JSON_COMMA) { j->error = "Found unexpected comma"; return nullptr; } - if (cc->type == JSON_HASH) { - cc->expect = JSON_KEY; + if (f->container->type == JSON_HASH) { + f->expect = JSON_KEY; } else { - cc->expect = JSON_ITEM; + f->expect = JSON_ITEM; } } @@ -390,18 +391,18 @@ again: /////////////////////////// Colon case ':': { - json_object *cc = current_container(j); - if (cc == nullptr) { + json_pull::parse_frame *f = current_frame(j); + if (f == nullptr) { j->error = "Found colon at top level"; return nullptr; } - if (cc->expect != JSON_COLON) { + if (f->expect != JSON_COLON) { j->error = "Found unexpected colon"; return nullptr; } - cc->expect = JSON_VALUE; + f->expect = JSON_VALUE; if (cb != nullptr) { cb(JSON_COLON, j, state); diff --git a/jsonpull/jsonpull.h b/jsonpull/jsonpull.h index b4ac51a6..e21a656d 100644 --- a/jsonpull/jsonpull.h +++ b/jsonpull/jsonpull.h @@ -72,7 +72,6 @@ struct json_object { json_pull *parser = nullptr; 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, json_object *p, json_pull *pl) : parent(p), parser(pl), type(t) {} @@ -190,10 +189,17 @@ struct json_pull { ssize_t buffer_head = 0; // Stack of currently-open containers; the top is the innermost container - // being parsed. Replaces the previous single `container` pointer / parent - // walk, which previously required enable_shared_from_this - // on every json_object instance (16 extra bytes per node). - std::vector container_stack; + // being parsed. Each frame also remembers what token is expected next + // (an item, a comma, a key, a colon, or a value). This stack is the + // only place the parser-only `expect` state lives, so it does not + // pollute json_object once parsing finishes. Replaces the previous + // single `container` pointer / parent walk, which previously required + // enable_shared_from_this on every json_object instance. + struct parse_frame { + json_object_ptr container; + json_type expect; + }; + std::vector container_stack; json_object_ptr root; std::string number_buffer;