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,