From ed45376fc41709bf2142d47d19b4fe68945b284e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 14 Oct 2026 16:59:31 -0700 Subject: [PATCH] Prove intact containers for JSON.stringify without reading their members Only JSON primitives and undefined now count as inert stores (published immortal containers may hold functions, accessors or prototypes), so an intact container provably holds only primitives and containers of its own region. The plain stringify proof then needs no member inspection: O(1) for arrays and a key scan for objects, instead of a pass that loaded every member node (a cache miss per element) before the write loaded it again. --- packages/runtime/src/scr_dyn.h | 27 +++++++++------ packages/runtime/src/scr_json.c | 60 +++++++++++---------------------- 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 306800061..db1502d4c 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -393,9 +393,11 @@ _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref * first store into any of its containers that could close a cycle. * * Invariant: while a region is intact, every outgoing edge of each of its - * containers is an edge the parser created, and each such edge leads to a - * node the parser created later in the same region (a container of the - * region, or a primitive). Following edges therefore strictly increases + * containers is an edge the parser created or a store of a primitive, and + * each parser edge leads to a node the parser created later in the same + * region (a container of the region, or a primitive). Containers have no + * prototype, accessors, symbol or named-array tables, and every property + * is enumerable. Following edges therefore strictly increases * creation order, so no node of an intact region lies on a cycle — whatever * points INTO the region from outside, including containers of other * regions. Releases of intact containers skip candidate buffering. @@ -418,10 +420,11 @@ _Static_assert(sizeof(((ScrDyn *)0)->v.arr) <= sizeof(((ScrDyn *)0)->v.typed_ref * Writers call scr_dyn_region_store(container, value) where they store a * reference into an existing array or object (items, entries, accessors, * prototype, named array properties, symbol tables). Removals, permutations - * of a container's own items, and stores of primitives or immortal values - * (which reach no mortal node) keep the region intact. Published graphs are - * immortal and frozen, so their releases never get here and they are never - * written. Regions are per thread like the nodes that point at them. */ + * of a container's own items, and stores of JSON primitives or undefined + * keep the region intact; accessor and property definitions never do. + * Published graphs are immortal and frozen, so their releases never get + * here and they are never written. Regions are per thread like the nodes + * that point at them. */ typedef struct ScrDynRegion { size_t refs; /* containers tagged with it, plus the parser while it runs */ bool intact; @@ -431,10 +434,14 @@ static inline ScrDynRegion *scr_dyn_region_of(const ScrDyn *d) { return d->kind == SCR_DYN_ARR ? d->v.arr.region : d->kind == SCR_DYN_OBJ ? d->v.obj.region : NULL; } -/* A value that can be stored without closing a cycle: no outgoing dyn - * edges (primitive kinds), or immortal (it reaches only immortal nodes). */ +/* A value that can be stored without closing a cycle or changing what the + * region holds: the JSON primitive kinds and undefined. Everything an + * intact container reaches is therefore a container of its region or one + * of these, which JSON.stringify relies on (scr_json_plain_container). + * Immortal containers (published graphs) are not inert: they may hold + * functions, accessors or prototypes. */ static inline bool scr_dyn_region_inert(const ScrDyn *value) { - return value && (value->rc == SIZE_MAX || value->kind <= SCR_DYN_STR || value->kind == SCR_DYN_UNDEF); + return value && (value->kind <= SCR_DYN_STR || value->kind == SCR_DYN_UNDEF); } /* Write barrier: `value` (NULL when unknown) is about to be stored into diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index bb432d15e..a002afef2 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8022,60 +8022,40 @@ static bool scr_json_plain_value(const ScrDyn *value) { } } -/* A container member whose SerializeJSONProperty step is a plain read with - * nothing to convert: arrays are never asked for toJSON here, and an object - * without a prototype of its own and without an own `toJSON` name resolves - * it to undefined (Object.prototype's members cannot be changed). */ -static bool scr_json_plain_child(const ScrDyn *value) { - if (value->kind == SCR_DYN_ARR) return true; - if (value->kind != SCR_DYN_OBJ || value->prototype || value == scr_builtin_object_prototype) return false; - for (size_t i = 0; i < value->v.obj.len; i++) { - const ScrDynEntry *entry = &value->v.obj.entries[i]; - if (entry->key_len == 6 && memcmp(entry->key, "toJSON", 6) == 0) return false; - } - return true; -} - /* Prove the whole immediate container before writing any of it. Without a * replacer or executable children, entries cannot change and borrowed names * need neither a key snapshot nor repeated lookups. Numeric property names * keep the ordinary sorted-key walk; sparse/accessor arrays keep Get. This * proof is repeated per call, so later mutations never leave a stale fact. * - * Container members are admitted only inside an intact parse region - * (ScrDynRegion): everything such a container reaches was built by the - * parser and holds JSON values only — no functions, accessors, prototypes - * or capsules — so writing a member subtree runs no program code that could - * mutate this container mid-walk, and no cycle passes through it (the - * circular-structure check has nothing to find). Each member container is - * still written through scr_json_replaced_write, which proves it afresh. - * Returns 0 (not plain), 1 (plain values only) or 2 (with container - * members). */ -static int scr_json_plain_container(const ScrDyn *value) { + * A container of an intact parse region (ScrDynRegion) holds only JSON + * primitives, undefined and containers of its own region, which have no + * prototype, accessors or toJSON function (a toJSON member there is never + * callable, which SerializeJSONProperty ignores). So its members need no + * inspection, which also keeps the proof from touching every member node: + * writing a member subtree runs no program code that could mutate this + * container mid-walk, and no cycle passes through it (the circular-structure + * check has nothing to find). Each member container is still written through + * scr_json_replaced_write, which proves it afresh. */ +static bool scr_json_plain_container(const ScrDyn *value) { const ScrDynRegion *region = scr_dyn_region_of(value); - bool nested_ok = region && region->intact; - bool nested = false; + bool intact = region && region->intact; if (value->kind == SCR_DYN_ARR) { - if (value->v.arr.presence || value->v.arr.properties) return 0; - for (size_t i = 0; i < value->v.arr.len; i++) { - const ScrDyn *item = value->v.arr.items[i]; - if (scr_json_plain_value(item)) continue; - if (!nested_ok || !scr_json_plain_child(item)) return 0; - nested = true; - } - return nested ? 2 : 1; + if (value->v.arr.presence || value->v.arr.properties) return false; + if (intact) return true; + for (size_t i = 0; i < value->v.arr.len; i++) + if (!scr_json_plain_value(value->v.arr.items[i])) return false; + return true; } - if (value->kind != SCR_DYN_OBJ) return 0; + if (value->kind != SCR_DYN_OBJ) return false; for (size_t i = 0; i < value->v.obj.len; i++) { const ScrDynEntry *entry = &value->v.obj.entries[i]; if (!entry->enumerable) continue; double index; - if (entry->accessor || scr_dyn_key_is_index(entry->key, entry->key_len, &index)) return 0; - if (scr_json_plain_value(entry->value)) continue; - if (!nested_ok || !scr_json_plain_child(entry->value)) return 0; - nested = true; + if (entry->accessor || (!intact && !scr_json_plain_value(entry->value)) || + scr_dyn_key_is_index(entry->key, entry->key_len, &index)) return false; } - return nested ? 2 : 1; + return true; } static bool scr_json_replaced_write(ScrJsonBuf *buffer, const ScrDyn *value,