From 11940ddb0431c516c9ea054e7c3ba621bd7f76ff Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:31:15 -0500 Subject: [PATCH] Introduce Object storage abstraction (#735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an opaque Object type for the key→value storage backing Value::Object. It exposes a small set of methods (get, insert, remove, iter, iter_sorted, cursor, serde) and keeps the backing store private, so future representations -- inline small-map, hash-backed, lazy, arena, FFI-callback -- can plug in without touching the call sites that name this type. Nothing in the engine uses Object yet. Value::Object still wraps Rc>; the payload swap and call-site migration come in the next PR. Object stands on its own unit tests in the meantime. docs/value/object.md walks through the design, the precedents it follows (serde_json::Map, toml::Table, simdjson DOM), and the concrete workloads the abstraction is meant to unlock. A matching Set abstraction follows in a separate PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/value/object.md | 84 +++++ src/lib.rs | 2 +- src/scheduler.rs | 6 +- src/{value.rs => value/mod.rs} | 12 + src/value/object/iter.rs | 148 +++++++++ src/value/object/mod.rs | 252 +++++++++++++++ src/value/object/serde.rs | 59 ++++ src/value/tests.rs | 564 +++++++++++++++++++++++++++++++++ 8 files changed, 1123 insertions(+), 4 deletions(-) create mode 100644 docs/value/object.md rename src/{value.rs => value/mod.rs} (99%) create mode 100644 src/value/object/iter.rs create mode 100644 src/value/object/mod.rs create mode 100644 src/value/object/serde.rs create mode 100644 src/value/tests.rs diff --git a/docs/value/object.md b/docs/value/object.md new file mode 100644 index 0000000..a71194d --- /dev/null +++ b/docs/value/object.md @@ -0,0 +1,84 @@ +# Object + +Opaque container for `Value::Object`'s key→value storage, enabling +alternative backends without call-site changes. + +## Design + +`Object` wraps the storage for a key→value collection of `Value`s and +provides a curated set of methods (`get`, `insert`, `remove`, `iter`, +`iter_sorted`, `cursor`, serde). The backing store is private; callers +never see or pattern-match on it, so the representation can change +without rippling through call sites. + +Multiple backends can coexist at runtime. Because the backing store is +private, different `Object` instances in the same process can use +different implementations — e.g., a lazy DB-backed object for `input`, +inline small-map objects for SARIF location records, and a regular +sorted map elsewhere — all interoperating through the same opaque +type. This is stronger than the typical Cargo-feature-selected backend +seen in precedent crates. + +Iteration is split intentionally. `iter()` makes no ordering promise, +which lets backends that don't keep entries sorted skip any sort work. +`iter_sorted()` returns entries in `Value` order and is what +serialization and `Ord` rely on for deterministic output. Cursor types +add resumable, incremental traversal for the RVM iteration state +without leaking iterator internals. + +`Ord` and `PartialOrd` are defined against `iter_sorted()` rather than +derived from the storage. Two `Object`s built on different backends — +or with different insertion histories — compare equal whenever their +sorted entries match, so changing the backend never changes observable +comparison results. + +## Precedents + +Other crates that hide storage behind a stable API so the implementation +can change without breaking callers: + +- **`serde_json::Map`** — opaque newtype allowing cargo-feature based + swap between `BTreeMap` (canonical order) and `IndexMap` (insertion + order). +- **`toml::Table`** — opaque newtype allowing cargo-feature based swap + between `BTreeMap` and `IndexMap`. +- **`simdjson` DOM** — opaque tree that lazily materializes nodes on + access instead of parsing the whole document up front. + +## Use cases + +- **SARIF small-object pressure** — SARIF reports contain millions of + small objects (location records, rule references, message arguments), + most with 2-5 keys. A small-map-optimized backend (inline storage + for ≤N entries, heap above) eliminates per-object BTreeMap allocation + for the common case. + +- **Kubernetes admission policies** — large, deeply-nested resource + objects (Pod specs, CRDs) where policies typically touch a handful + of paths. A lazy-materializing backend (`LazyObjectProvider` over + the incoming JSON) parses only the accessed subtrees. + +- **Azure Policy aliases** — ARM exposes the same logical property + under multiple aliases (e.g. paths like + `Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.id`). + An alias-aware backend resolves lookups across canonical and alias + forms without rewriting every policy. + +- **Azure Policy case-insensitive compare** — ARM property names are + case-preserving but case-insensitive on lookup (`tags.Environment` + and `tags.environment` resolve identically). A case-insensitive + backend centralizes this once at the storage layer instead of at + every comparison site. + +- **External data sources** — `input` or `data` backed by a database + query, CBOR slice, REST endpoint, or other streaming source via a + `LazyObjectProvider`. Entries materialize on demand; the policy + only pays for what it touches. + +- **Eval-time temporaries** — objects constructed during evaluation + (comprehensions, intermediate rule results) on a bumpalo arena. + The whole arena drops at query end with zero per-entry free cost. + +- **Host-language interop** — Python dicts or JS objects accessed via + FFI callbacks from the embedding application, without copying into + Rust on every binding boundary. diff --git a/src/lib.rs b/src/lib.rs index 0056fa0..0081580 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,7 +155,7 @@ pub mod target; #[cfg(any(test, all(feature = "yaml", feature = "std")))] pub mod test_utils; pub mod utils; -mod value; +pub mod value; #[cfg(feature = "azure_policy")] pub use { diff --git a/src/scheduler.rs b/src/scheduler.rs index d1d8332..a54774d 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -569,7 +569,7 @@ impl Analyzer { } Ok(false) } - Array { .. } | Object { .. } => Ok(true), + Expr::Array { .. } | Expr::Object { .. } => Ok(true), _ => Ok(false), })?; Ok(true) @@ -666,7 +666,7 @@ impl Analyzer { Ok(false) } // TODO: key vs value for object binding - Array { .. } | Object { .. } => Ok(true), + Expr::Array { .. } | Expr::Object { .. } => Ok(true), _ => Ok(false), })?; Ok(vars) @@ -853,7 +853,7 @@ impl Analyzer { Ok(false) } // TODO: Object key/value - Array { .. } | Object { .. } => Ok(true), + Expr::Array { .. } | Expr::Object { .. } => Ok(true), _ => { non_vars.push(e.clone()); Ok(false) diff --git a/src/value.rs b/src/value/mod.rs similarity index 99% rename from src/value.rs rename to src/value/mod.rs index 7690865..678309c 100644 --- a/src/value.rs +++ b/src/value/mod.rs @@ -11,6 +11,18 @@ clippy::as_conversions )] // value helpers index paths directly for performance +mod object; + +#[cfg(test)] +mod tests; + +#[allow(unused_imports)] // surface for downstream PRs +pub use object::{IntoIter, Iter, IterMut, Object}; + +#[cfg(feature = "rvm")] +#[allow(unused_imports)] // surface for downstream PRs +pub use object::ObjectCursor; + use crate::number::Number; use alloc::collections::{BTreeMap, BTreeSet}; diff --git a/src/value/object/iter.rs b/src/value/object/iter.rs new file mode 100644 index 0000000..609f9be --- /dev/null +++ b/src/value/object/iter.rs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Opaque iterator types for [`Object`]. +//! +//! These newtypes wrap the storage backend's iterators so the backend can be +//! swapped without changing any iterator type signatures observed by callers. + +use alloc::collections::btree_map; +use core::iter::FusedIterator; + +use super::Object; +use crate::value::Value; + +/// Owned iterator over `(Value, Value)` entries. +#[derive(Debug)] +pub struct IntoIter { + pub(super) inner: btree_map::IntoIter, +} + +impl Iterator for IntoIter { + type Item = (Value, Value); + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl DoubleEndedIterator for IntoIter { + #[inline] + fn next_back(&mut self) -> Option { + self.inner.next_back() + } +} + +impl ExactSizeIterator for IntoIter { + #[inline] + fn len(&self) -> usize { + self.inner.len() + } +} + +impl FusedIterator for IntoIter {} + +/// Borrowed iterator over `(&Value, &Value)` entries. +#[derive(Debug, Clone)] +pub struct Iter<'a> { + pub(super) inner: btree_map::Iter<'a, Value, Value>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a Value, &'a Value); + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl<'a> DoubleEndedIterator for Iter<'a> { + #[inline] + fn next_back(&mut self) -> Option { + self.inner.next_back() + } +} + +impl<'a> ExactSizeIterator for Iter<'a> { + #[inline] + fn len(&self) -> usize { + self.inner.len() + } +} + +impl<'a> FusedIterator for Iter<'a> {} + +/// Borrowed iterator over `(&Value, &mut Value)` entries. +#[derive(Debug)] +pub struct IterMut<'a> { + pub(super) inner: btree_map::IterMut<'a, Value, Value>, +} + +impl<'a> Iterator for IterMut<'a> { + type Item = (&'a Value, &'a mut Value); + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl<'a> DoubleEndedIterator for IterMut<'a> { + #[inline] + fn next_back(&mut self) -> Option { + self.inner.next_back() + } +} + +impl<'a> ExactSizeIterator for IterMut<'a> { + #[inline] + fn len(&self) -> usize { + self.inner.len() + } +} + +impl<'a> FusedIterator for IterMut<'a> {} + +impl IntoIterator for Object { + type Item = (Value, Value); + type IntoIter = IntoIter; + #[inline] + fn into_iter(self) -> Self::IntoIter { + IntoIter { + inner: self.inner.into_iter(), + } + } +} + +impl<'a> IntoIterator for &'a Object { + type Item = (&'a Value, &'a Value); + type IntoIter = Iter<'a>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + Iter { + inner: self.inner.iter(), + } + } +} + +impl<'a> IntoIterator for &'a mut Object { + type Item = (&'a Value, &'a mut Value); + type IntoIter = IterMut<'a>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + IterMut { + inner: self.inner.iter_mut(), + } + } +} diff --git a/src/value/object/mod.rs b/src/value/object/mod.rs new file mode 100644 index 0000000..0592124 --- /dev/null +++ b/src/value/object/mod.rs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! See [`Object`]. + +mod iter; +mod serde; + +use alloc::collections::BTreeMap; +use core::cmp::Ordering; +use core::fmt; +use core::ops::Bound; + +use crate::value::Value; + +pub use iter::{IntoIter, Iter, IterMut}; + +/// Opaque, ordered key-value map keyed by [`Value`]. +/// +/// The current backing storage is `BTreeMap`. The inner field +/// is private so the representation can change (two-tier inline+hash, lazy, +/// schema-shared) without touching call sites. +/// +/// # Iteration +/// +/// - [`Object::iter`] — implementation-defined order; non-resumable. +/// - [`Object::iter_sorted`] — sorted by `Value::Ord`; non-resumable. +/// - [`Object::cursor`] / [`Object::next`] — implementation-defined order, +/// resumable; cheapest per-step cost. Used by interpreter/RVM when iteration +/// must yield mid-flight. +#[derive(Default, Clone, Eq, PartialEq)] +pub struct Object { + inner: BTreeMap, +} + +impl Object { + /// Create an empty `Object`. + #[inline] + pub const fn new() -> Self { + Self { + inner: BTreeMap::new(), + } + } + + #[inline] + pub fn len(&self) -> usize { + self.inner.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + #[inline] + pub fn get(&self, key: &Value) -> Option<&Value> { + self.inner.get(key) + } + + #[inline] + pub fn contains_key(&self, key: &Value) -> bool { + self.inner.contains_key(key) + } + + #[inline] + pub fn get_mut(&mut self, key: &Value) -> Option<&mut Value> { + self.inner.get_mut(key) + } + + /// Iteration in implementation-defined order. Non-resumable. + /// + /// For the current BTree-backed storage this happens to be sorted, but + /// callers MUST NOT depend on that. Use [`Object::iter_sorted`] when + /// deterministic order is required, or [`Object::cursor`] when iteration + /// must yield and resume. + #[inline] + pub fn iter(&self) -> impl Iterator + '_ { + self.inner.iter() + } + + /// Iteration in sorted key order (by `Value::Ord`). Non-resumable. + /// + /// Use this for serialization, snapshots, hashing, `Debug`, the + /// `object.keys` builtin, etc. + #[inline] + pub fn iter_sorted(&self) -> Iter<'_> { + // BTree backend iterates sorted natively. + Iter { + inner: self.inner.iter(), + } + } + + #[inline] + pub fn keys(&self) -> impl Iterator + '_ { + self.inner.keys() + } + + /// Keys in sorted order (by `Value::Ord`). Symmetric with + /// [`Object::iter_sorted`]. + #[inline] + pub fn keys_sorted(&self) -> impl Iterator + '_ { + self.iter_sorted().map(|(k, _)| k) + } + + #[inline] + pub fn values(&self) -> impl Iterator + '_ { + self.inner.values() + } + + #[inline] + pub fn iter_mut(&mut self) -> IterMut<'_> { + IterMut { + inner: self.inner.iter_mut(), + } + } + + /// Insert a key-value pair. Returns the previous value if any. + #[inline] + pub fn insert(&mut self, key: Value, value: Value) -> Option { + self.inner.insert(key, value) + } + + #[inline] + pub fn remove(&mut self, key: &Value) -> Option { + self.inner.remove(key) + } + + #[inline] + pub fn retain(&mut self, f: F) + where + F: FnMut(&Value, &mut Value) -> bool, + { + self.inner.retain(f); + } + + #[inline] + pub fn clear(&mut self) { + self.inner.clear(); + } + + #[inline] + pub fn append(&mut self, other: &mut Object) { + self.inner.append(&mut other.inner); + } + + /// Gets a mutable reference to the value associated with `key`, inserting + /// the result of `default()` if absent. Single O(log n) probe. + pub fn get_or_insert_with Value>( + &mut self, + key: Value, + default: F, + ) -> &mut Value { + self.inner.entry(key).or_insert_with(default) + } + + /// Create a resumable cursor over entries in implementation-defined + /// order. Stable for the lifetime of `&self`. O(1). + /// + /// The cursor is fully self-owned (it stores a clone of the last-seen + /// key, not a reference) so it can be stored as a field of a + /// long-lived state struct — e.g. an RVM iteration frame that persists + /// across instruction dispatches. As a consequence, mutating the + /// `Object` between `next()` calls is not rejected by the borrow + /// checker; the resulting iteration order in that case is unspecified. + #[inline] + pub const fn cursor(&self) -> ObjectCursor { + ObjectCursor { + inner: ObjectCursorInner::BTree(None), + } + } + + /// Advance `cursor` and yield the next entry. O(log n) for the BTree + /// backend (range probe); future hash/inline variants may be O(1). + pub fn next<'a>(&'a self, cursor: &mut ObjectCursor) -> Option<(&'a Value, &'a Value)> { + let ObjectCursorInner::BTree(ref mut last) = cursor.inner; + let next = last.as_ref().map_or_else( + || self.inner.iter().next(), + |prev| { + self.inner + .range((Bound::Excluded(prev.clone()), Bound::Unbounded)) + .next() + }, + ); + let (k, v) = next?; + *last = Some(k.clone()); + Some((k, v)) + } +} + +/// Opaque resumable cursor over an [`Object`]'s entries in +/// implementation-defined order. +/// +/// Self-owned: holds no borrow on the `Object`, so it can be stored as a +/// field of a long-lived state struct (e.g. an RVM iteration frame). +#[derive(Debug, Clone)] +pub struct ObjectCursor { + inner: ObjectCursorInner, +} + +#[derive(Debug, Clone)] +enum ObjectCursorInner { + /// BTree backend cursor: tracks last-seen key. `None` means "before start". + BTree(Option), +} + +// ---- Hand-written Ord/PartialOrd ---------------------------------------- +// +// Implemented in terms of `iter_sorted()` so ordering is consistent with the +// canonical (sorted) view of the entries and is therefore independent of +// the storage variant. + +impl Ord for Object { + fn cmp(&self, other: &Self) -> Ordering { + self.iter_sorted().cmp(other.iter_sorted()) + } +} + +impl PartialOrd for Object { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl fmt::Debug for Object { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Use sorted iteration so Debug output is stable across storage + // variants. + f.debug_map().entries(self.iter_sorted()).finish() + } +} + +impl Extend<(Value, Value)> for Object { + fn extend>(&mut self, iter: I) { + self.inner.extend(iter); + } +} + +impl FromIterator<(Value, Value)> for Object { + fn from_iter>(iter: I) -> Self { + Self { + inner: BTreeMap::from_iter(iter), + } + } +} + +impl From> for Object { + #[inline] + fn from(map: BTreeMap) -> Self { + Self { inner: map } + } +} diff --git a/src/value/object/serde.rs b/src/value/object/serde.rs new file mode 100644 index 0000000..dc0be2f --- /dev/null +++ b/src/value/object/serde.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Serde `Serialize`/`Deserialize` impls for [`Object`]. + +use alloc::string::ToString as _; +use core::fmt; + +use serde::de::{Deserialize, Deserializer, Error as _, MapAccess, Visitor}; +use serde::ser::{Serialize, SerializeMap as _, Serializer}; + +use super::Object; +use crate::value::Value; + +impl Serialize for Object { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::Error; + let mut map = serializer.serialize_map(Some(self.len()))?; + // Sorted iteration: canonical JSON. + for (k, v) in self.iter_sorted() { + match *k { + Value::String(_) => map.serialize_entry(k, v)?, + _ => { + // Non-string keys are stringified via serde_json::to_string + // so the resulting JSON has valid string keys. + let key_str = serde_json::to_string(k).map_err(Error::custom)?; + map.serialize_entry(&key_str, v)?; + } + } + } + map.end() + } +} + +struct ObjectVisitor; + +impl<'de> Visitor<'de> for ObjectVisitor { + type Value = Object; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("a map of Value to Value") + } + + fn visit_map>(self, mut access: A) -> Result { + let mut obj = Object::new(); + while let Some((k, v)) = access.next_entry::()? { + obj.insert(k, v); + crate::utils::limits::check_memory_limit_if_needed() + .map_err(|err| A::Error::custom(err.to_string()))?; + } + Ok(obj) + } +} + +impl<'de> Deserialize<'de> for Object { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_map(ObjectVisitor) + } +} diff --git a/src/value/tests.rs b/src/value/tests.rs new file mode 100644 index 0000000..57ba0af --- /dev/null +++ b/src/value/tests.rs @@ -0,0 +1,564 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow( + clippy::expect_used, + clippy::unwrap_used, + clippy::indexing_slicing, + clippy::as_conversions, + clippy::arithmetic_side_effects, + clippy::unseparated_literal_suffix, + clippy::map_unwrap_or, + clippy::option_if_let_else, + clippy::pattern_type_mismatch +)] + +use alloc::collections::BTreeMap; +use alloc::format; +use alloc::vec::Vec; + +use super::Object; +use crate::value::Value; + +fn val(i: u64) -> Value { + Value::from(i) +} + +fn make_pairs(n: u64) -> Vec<(Value, Value)> { + (0..n).map(|i| (val(i), val(i.saturating_mul(2)))).collect() +} + +const SIZES: &[u64] = &[0, 1, 2, 4, 8, 64, 256, 1024]; + +/// `iter_sorted` must yield entries in the same order as a `BTreeMap` oracle. +#[test] +fn object_iter_sorted_matches_btreemap_oracle() { + for &n in SIZES { + let pairs = make_pairs(n); + let oracle: BTreeMap = pairs.iter().cloned().collect(); + let obj: Object = pairs.into_iter().collect(); + let actual: Vec<(&Value, &Value)> = obj.iter_sorted().collect(); + let expected: Vec<(&Value, &Value)> = oracle.iter().collect(); + assert_eq!(actual, expected, "size {n}"); + } +} + +/// `iter` may be in any order, but as a multiset must equal the oracle's entries. +#[test] +fn object_iter_multiset_equality_with_oracle() { + for &n in SIZES { + let pairs = make_pairs(n); + let oracle: BTreeMap = pairs.iter().cloned().collect(); + let obj: Object = pairs.into_iter().collect(); + assert_eq!(obj.len(), oracle.len(), "size {n}"); + let mut a: Vec<(Value, Value)> = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let mut b: Vec<(Value, Value)> = + oracle.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + a.sort(); + b.sort(); + assert_eq!(a, b); + } +} + +/// Serialize-then-deserialize must round-trip through JSON without loss. +#[test] +fn object_serde_roundtrip() { + for &n in &[0_u64, 1, 8, 64] { + let pairs: Vec<(Value, Value)> = (0..n) + .map(|i| (Value::String(format!("k{i}").into()), val(i))) + .collect(); + let obj: Object = pairs.into_iter().collect(); + let json = serde_json::to_string(&obj).expect("ser"); + let back: Object = serde_json::from_str(&json).expect("de"); + assert_eq!(obj, back, "size {n}"); + } +} + +/// Equality depends only on contents, not the order keys were inserted. +#[test] +fn object_eq_invariant_to_insertion_order() { + let mut a = Object::new(); + let mut b = Object::new(); + for i in 0..32_u64 { + a.insert(val(i), val(i.saturating_add(1))); + } + for i in (0..32_u64).rev() { + b.insert(val(i), val(i.saturating_add(1))); + } + assert_eq!(a, b); +} + +/// `remove` returns the prior value (or `None`) and `retain` keeps only matching entries. +#[test] +fn object_remove_and_retain() { + let mut obj: Object = make_pairs(16).into_iter().collect(); + assert_eq!(obj.remove(&val(0)), Some(val(0))); + assert!(obj.remove(&val(100)).is_none()); + obj.retain(|_, v| { + if let Value::Number(ref n) = *v { + n.as_u64().is_some_and(|x| x % 4 == 0) + } else { + false + } + }); + for (_, v) in obj.iter_sorted() { + if let Value::Number(ref n) = *v { + assert_eq!(n.as_u64().expect("u64") % 4, 0); + } + } +} + +/// `IntoIterator` for `Object` (by value) yields every entry exactly once. +#[test] +fn object_into_iterator_owned() { + let obj: Object = make_pairs(8).into_iter().collect(); + let collected: Vec<(Value, Value)> = obj.into_iter().collect(); + assert_eq!(collected.len(), 8); +} + +// ---- Duplicate-key semantics -------------------------------------------- + +/// `FromIterator` keeps the last value when the same key appears multiple times. +#[test] +fn object_from_iter_last_wins_on_duplicate_keys() { + let obj = Object::from_iter([(val(0), val(1)), (val(0), val(2))]); + assert_eq!(obj.get(&val(0)), Some(&val(2))); + assert_eq!(obj.len(), 1); +} + +/// `From` adopts `BTreeMap`'s own last-write-wins semantics for duplicates. +#[test] +fn object_from_btreemap_last_wins_on_duplicate_keys() { + let mut bm: BTreeMap = BTreeMap::new(); + bm.insert(val(0), val(1)); + bm.insert(val(0), val(2)); + let obj: Object = bm.into(); + assert_eq!(obj.get(&val(0)), Some(&val(2))); + assert_eq!(obj.len(), 1); +} + +// ---- get_or_insert_with -------------------------------------------------- + +/// `get_or_insert_with` inserts the default when the key is absent and returns a mutable ref to it. +#[test] +fn object_get_or_insert_with_inserts_when_absent() { + let mut obj = Object::new(); + let v = obj.get_or_insert_with(val(7), || val(42)); + assert_eq!(*v, val(42)); + *v = val(43); + assert_eq!(obj.get(&val(7)), Some(&val(43))); +} + +/// `get_or_insert_with` returns the existing value and never invokes the default closure. +#[test] +fn object_get_or_insert_with_returns_existing_when_present() { + let mut obj = Object::new(); + obj.insert(val(7), val(1)); + let mut closure_called = false; + let v = obj.get_or_insert_with(val(7), || { + closure_called = true; + val(999) + }); + assert_eq!(*v, val(1)); + assert!(!closure_called, "default closure must not run when present"); +} + +// ---- Accessor coverage --------------------------------------------------- + +/// Smoke-test every accessor: `contains_key`/`get`/`get_mut`/`keys`/`values`/`iter`/`iter_mut`/`append`/`clear`. +#[test] +fn object_accessor_coverage() { + let mut obj: Object = make_pairs(4).into_iter().collect(); + + assert!(obj.contains_key(&val(0))); + assert!(!obj.contains_key(&val(100))); + + assert_eq!(obj.get(&val(2)), Some(&val(4))); + + if let Some(v) = obj.get_mut(&val(1)) { + *v = val(999); + } + assert_eq!(obj.get(&val(1)), Some(&val(999))); + + let keys: Vec<&Value> = obj.keys().collect(); + assert_eq!(keys.len(), 4); + let values: Vec<&Value> = obj.values().collect(); + assert_eq!(values.len(), 4); + + for (_, v) in obj.iter_mut() { + *v = val(0); + } + for (_, v) in obj.iter() { + assert_eq!(*v, val(0)); + } + + let mut other = Object::new(); + other.insert(val(100), val(200)); + obj.append(&mut other); + assert!(other.is_empty()); + assert!(obj.contains_key(&val(100))); + + obj.clear(); + assert!(obj.is_empty()); +} + +// ---- IntoIterator for references ----------------------------------------- + +/// `IntoIterator` for `&Object` yields shared refs to every entry. +#[test] +fn object_into_iterator_ref() { + let obj: Object = make_pairs(4).into_iter().collect(); + let mut count = 0; + for (_k, _v) in &obj { + count += 1; + } + assert_eq!(count, 4); +} + +/// `IntoIterator` for `&mut Object` exposes mutable refs to values; mutations persist. +#[test] +fn object_into_iterator_ref_mut() { + let mut obj: Object = make_pairs(4).into_iter().collect(); + for (_k, v) in &mut obj { + *v = val(0); + } + for (_, v) in obj.iter() { + assert_eq!(*v, val(0)); + } +} + +// ---- Cursor tests -------------------------------------------------------- + +/// Driving `cursor`+`next` to completion visits each entry exactly once. +#[test] +fn object_cursor_yields_every_entry_once() { + for &n in SIZES { + let pairs = make_pairs(n); + let obj: Object = pairs.clone().into_iter().collect(); + let mut cursor = obj.cursor(); + let mut collected: Vec<(Value, Value)> = Vec::new(); + while let Some((k, v)) = obj.next(&mut cursor) { + collected.push((k.clone(), v.clone())); + } + let mut a = collected; + a.sort(); + let mut b = pairs; + b.sort(); + assert_eq!(a, b, "size {n}"); + } +} + +/// A freshly-constructed cursor restarts from the beginning, independent of any prior cursor's state. +#[test] +fn object_cursor_resumable_fresh_cursor_restarts() { + let obj: Object = make_pairs(8).into_iter().collect(); + let mut c1 = obj.cursor(); + let _ = obj.next(&mut c1); + let _ = obj.next(&mut c1); + let mut c2 = obj.cursor(); + let first_again = obj.next(&mut c2); + let first_original = obj.iter().next(); + assert_eq!( + first_again.map(|(k, v)| (k.clone(), v.clone())), + first_original.map(|(k, v)| (k.clone(), v.clone())) + ); +} + +/// When `Object` is shared via `Rc`, `Rc::make_mut` clones — leaving an in-flight cursor on the original snapshot unaffected. +#[test] +fn object_cursor_snapshot_independence_via_rc() { + use crate::Rc; + let mut obj = Object::new(); + obj.insert(Value::from("a"), Value::from(1)); + obj.insert(Value::from("b"), Value::from(2)); + obj.insert(Value::from("c"), Value::from(3)); + let rc_obj = Rc::new(obj); + + let alias = Rc::clone(&rc_obj); + let mut cursor = rc_obj.cursor(); + let _ = rc_obj.next(&mut cursor); + + let mut alias_for_mut = alias; + Rc::make_mut(&mut alias_for_mut).insert(Value::from("d"), Value::from(4)); + Rc::make_mut(&mut alias_for_mut).remove(&Value::from("a")); + + assert_eq!(rc_obj.len(), 3); + let mut remaining = 0; + while rc_obj.next(&mut cursor).is_some() { + remaining += 1; + } + assert_eq!(remaining, 2); +} + +/// A cursor over an empty `Object` returns `None` on the first call. +#[test] +fn object_cursor_empty_returns_none_immediately() { + let obj = Object::new(); + let mut cursor = obj.cursor(); + assert!(obj.next(&mut cursor).is_none()); +} + +/// Mutating an `Object` between `next()` calls is well-defined: the cursor +/// must not panic and must terminate. The visit order, and whether +/// inserted/removed keys appear, is intentionally unspecified — this test +/// only pins the safety + termination guarantees that callers (e.g. a +/// future RVM iteration frame) may rely on. It must NOT assert any +/// particular order or count, or future backend swaps will be forced to +/// honor an accidental contract. +#[test] +fn object_cursor_mutation_between_steps_is_safe_and_terminates() { + let mut obj: Object = make_pairs(16).into_iter().collect(); + let mut cursor = obj.cursor(); + + // Yield a few entries before mutating. + for _ in 0..3 { + let _ = obj.next(&mut cursor); + } + + // Interleave mutations and steps. Each yielded entry must, at the + // moment of yield, be a real entry in the map. + obj.insert(val(100), val(100)); + if let Some((k, v)) = obj.next(&mut cursor) { + assert_eq!(obj.get(k), Some(v)); + } + obj.remove(&val(2)); + if let Some((k, v)) = obj.next(&mut cursor) { + assert_eq!(obj.get(k), Some(v)); + } + obj.clear(); + // After clear(), draining the cursor must terminate (not panic, not + // loop) within a bounded number of calls. + let mut terminated = false; + for _ in 0..32 { + if obj.next(&mut cursor).is_none() { + terminated = true; + break; + } + } + assert!(terminated, "cursor failed to terminate after clear()"); +} + +// ---- Hand-written Ord consistency --------------------------------------- + +/// `Ord` (built atop `iter_sorted`) is invariant to insertion order. +#[test] +fn object_ord_invariant_to_insertion_order() { + let mut a = Object::new(); + let mut b = Object::new(); + for i in 0..16_u64 { + a.insert(val(i), val(i.saturating_add(1))); + } + for i in (0..16_u64).rev() { + b.insert(val(i), val(i.saturating_add(1))); + } + use core::cmp::Ordering; + assert_eq!(a.cmp(&b), Ordering::Equal); +} + +/// `Ord` agrees with lexicographic comparison of the sorted-entries view. +#[test] +fn object_ord_lexicographic_on_sorted_entries() { + let a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect(); + let b: Object = [(val(0), val(0)), (val(2), val(2))].into_iter().collect(); + assert!(a < b); +} + +/// `empty < non_empty` and a shorter prefix compares less than its extension. +#[test] +fn object_ord_empty_and_prefix() { + use core::cmp::Ordering; + let empty = Object::new(); + let one: Object = [(val(0), val(0))].into_iter().collect(); + let two: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect(); + assert_eq!(empty.cmp(&one), Ordering::Less); + assert_eq!(one.cmp(&two), Ordering::Less); + assert_eq!(two.cmp(&empty), Ordering::Greater); +} + +/// When keys match, `Ord` falls through to comparing values. +#[test] +fn object_ord_breaks_ties_on_values() { + use core::cmp::Ordering; + let a: Object = [(val(0), val(1))].into_iter().collect(); + let b: Object = [(val(0), val(2))].into_iter().collect(); + assert_eq!(a.cmp(&b), Ordering::Less); +} + +/// `PartialOrd` must agree with `Ord` for every input pair. +#[test] +fn object_partial_cmp_matches_cmp() { + let a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect(); + let b: Object = [(val(0), val(0)), (val(2), val(2))].into_iter().collect(); + assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b))); + assert_eq!(b.partial_cmp(&a), Some(b.cmp(&a))); + assert_eq!(a.partial_cmp(&a), Some(core::cmp::Ordering::Equal)); +} + +// ---- Debug / keys_sorted determinism ------------------------------------ + +/// `Debug` output is byte-identical for equal Objects regardless of insertion order. +#[test] +fn object_debug_invariant_to_insertion_order() { + let mut a = Object::new(); + let mut b = Object::new(); + for i in 0..8_u64 { + a.insert(val(i), val(i)); + } + for i in (0..8_u64).rev() { + b.insert(val(i), val(i)); + } + assert_eq!(format!("{a:?}"), format!("{b:?}")); +} + +/// `keys_sorted` yields exactly `iter_sorted().map(|(k,_)| k)`. +#[test] +fn object_keys_sorted_matches_iter_sorted_keys() { + let obj: Object = make_pairs(16).into_iter().collect(); + let from_keys: Vec<&Value> = obj.keys_sorted().collect(); + let from_iter: Vec<&Value> = obj.iter_sorted().map(|(k, _)| k).collect(); + assert_eq!(from_keys, from_iter); +} + +// ---- Serde: non-string keys & determinism -------------------------------- + +/// `Serialize` stringifies non-string keys, and equal Objects produce identical JSON +/// regardless of insertion order. +#[test] +fn object_serialize_non_string_keys_and_deterministic() { + let pairs = [ + (Value::from("alpha"), val(1)), + (Value::Bool(true), val(2)), + (val(7), val(3)), + ]; + let a: Object = pairs.iter().cloned().collect(); + let mut b = Object::new(); + for (k, v) in pairs.iter().rev().cloned() { + b.insert(k, v); + } + let ja = serde_json::to_string(&a).expect("ser a"); + let jb = serde_json::to_string(&b).expect("ser b"); + assert_eq!(ja, jb, "serialization must be deterministic"); + + // Non-string keys appear as quoted strings in the resulting JSON. + let v: serde_json::Value = serde_json::from_str(&ja).expect("parse"); + let obj = v.as_object().expect("json object"); + assert!( + obj.contains_key("true"), + "bool key was not stringified: {ja}" + ); + assert!( + obj.contains_key("7"), + "number key was not stringified: {ja}" + ); + assert!(obj.contains_key("alpha")); +} + +// ---- Extend / append duplicate-key semantics ----------------------------- + +/// `extend` overwrites existing entries (last-write-wins) and preserves length when +/// only existing keys are touched. +#[test] +fn object_extend_last_wins_and_empty_noop() { + let mut obj: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect(); + obj.extend([(val(0), val(99))]); + assert_eq!(obj.get(&val(0)), Some(&val(99))); + assert_eq!(obj.len(), 2); + + let before = obj.len(); + obj.extend(core::iter::empty::<(Value, Value)>()); + assert_eq!(obj.len(), before, "empty extend is a no-op"); +} + +/// `append` drains `other` into `self`, overwriting on overlapping keys. +#[test] +fn object_append_overlapping_keys_drain_and_overwrite() { + let mut a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect(); + let mut b: Object = [(val(1), val(99)), (val(2), val(2))].into_iter().collect(); + a.append(&mut b); + assert!(b.is_empty(), "append must drain `other`"); + assert_eq!(a.len(), 3); + assert_eq!(a.get(&val(1)), Some(&val(99))); + assert_eq!(a.get(&val(2)), Some(&val(2))); +} + +// ---- Iterator trait surface --------------------------------------------- + +/// `DoubleEndedIterator`/`ExactSizeIterator`/`FusedIterator` and `size_hint` all +/// behave correctly across partial consumption from both ends. +#[test] +fn object_iter_sorted_double_ended_and_exact_size() { + let obj: Object = make_pairs(4).into_iter().collect(); + let mut it = obj.iter_sorted(); + assert_eq!(it.len(), 4); + assert_eq!(it.size_hint(), (4, Some(4))); + + let first = it.next().expect("front"); + let last = it.next_back().expect("back"); + assert_eq!(it.len(), 2); + assert_eq!(it.size_hint(), (2, Some(2))); + assert_ne!(first.0, last.0, "front and back must differ for n=4"); + + // Drain remaining. + while it.next().is_some() {} + assert_eq!(it.len(), 0); + // FusedIterator: stays None after exhaustion. + assert!(it.next().is_none()); + assert!(it.next().is_none()); + assert!(it.next_back().is_none()); +} + +/// `IntoIter` also honors `DoubleEndedIterator` and `ExactSizeIterator`. +#[test] +fn object_into_iter_double_ended_and_exact_size() { + let obj: Object = make_pairs(4).into_iter().collect(); + let mut it = obj.into_iter(); + assert_eq!(it.len(), 4); + let _ = it.next().expect("front"); + let _ = it.next_back().expect("back"); + assert_eq!(it.len(), 2); + let collected: Vec<_> = it.collect(); + assert_eq!(collected.len(), 2); +} + +/// `IterMut` decrements its `len()` after consuming from the front. +#[test] +fn object_iter_mut_exact_size() { + let mut obj: Object = make_pairs(3).into_iter().collect(); + let mut it = obj.iter_mut(); + assert_eq!(it.len(), 3); + let _ = it.next().expect("front"); + assert_eq!(it.len(), 2); +} + +/// `Iter` is `Clone`; the clone iterates independently from the same point. +#[test] +fn object_iter_sorted_clone_is_independent() { + let obj: Object = make_pairs(4).into_iter().collect(); + let mut a = obj.iter_sorted(); + let _ = a.next(); + let b = a.clone(); + let rest_a: Vec<_> = a.collect(); + let rest_b: Vec<_> = b.collect(); + assert_eq!(rest_a, rest_b); +} + +// ---- default / insert --------------------------------------------------- + +/// `Object::default()` and `Object::new()` produce equal, empty Objects. +#[test] +fn object_default_equals_new_and_is_empty() { + let a = Object::default(); + let b = Object::new(); + assert_eq!(a, b); + assert!(a.is_empty()); + assert_eq!(a.len(), 0); +} + +/// `insert` returns `None` for a fresh key and `Some(old)` when overwriting. +#[test] +fn object_insert_returns_previous_value() { + let mut obj = Object::new(); + assert_eq!(obj.insert(val(0), val(1)), None); + assert_eq!(obj.insert(val(0), val(2)), Some(val(1))); + assert_eq!(obj.get(&val(0)), Some(&val(2))); +}