Skip to content

Commit 93216b2

Browse files
anakrishCopilot
andcommitted
fix(value/array): tighten memory-limit & API-surface review findings
Addresses follow-up review on PR #63 plus our own strict re-review: - Drop the process-aborting Extend/FromIterator impls. They now match the Object convention: delegate directly to the inner container without consulting the cooperative memory limit (rely on the allocator to fail on true OOM). Limit-aware bulk insertion has explicit fallible methods. - Add Array::try_from_vec and Array::try_from_iter as the limit-aware alternatives to the From<Vec<Value>> / FromIterator pairs. - with_capacity now also consults check_memory_limit_if_needed after the inner try_reserve so capacity reservations cannot silently push usage past the cooperative ceiling. - insert now returns Result<()>: out-of-bounds and limit failures both flow through the same anyhow error channel. The limit case rolls back the inserted element so the array length is unchanged. - push and extend_from_slice roll back the pushed element(s) on a limit failure rather than leaving the array in a half-mutated state. - Make the array module itself private (was `pub mod array`); only `Array` (always) and `ArrayCursor` (rvm feature) are re-exported from `crate::value`. This matches the Object/ObjectCursor pattern and keeps the module layout out of the public API surface. - Rename the rvm-only Cursor type to ArrayCursor for symmetry with ObjectCursor and to disambiguate at the value-module level. - Update docs/value/array.md to reflect the private module, the ArrayCursor name, and the limit-enforcement contract. - Extend the test suite with try_from_vec, try_from_iter, and an insert out-of-bounds error case. Update existing insert call sites to the Result API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 25ba655 commit 93216b2

5 files changed

Lines changed: 147 additions & 41 deletions

File tree

docs/value/array.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ shared design philosophy.
77
## Design
88

99
`Array` wraps a `Vec<Value>` today, but the inner vector is private: callers
10-
cannot pattern-match it or take ownership of the backing store. The public API is
11-
the curated surface in `src/value/array/mod.rs`, grouped roughly as:
10+
cannot pattern-match it or take ownership of the backing store. The module
11+
itself is private (`mod array;` in `value/mod.rs`); `Array` (and the
12+
`rvm`-gated `ArrayCursor`) are the only types re-exported, so the module
13+
layout is not part of the public API. The curated surface in
14+
`src/value/array/mod.rs` groups roughly as:
1215

13-
- construction/conversion: `new`, `with_capacity`, `From<Vec<Value>>`,
14-
`FromIterator<Value>`, `IntoIterator`, `From<Array> for Value`, `into_value`
16+
- construction/conversion: `new`, `with_capacity`, `try_from_vec`,
17+
`try_from_iter`, `From<Vec<Value>>`, `FromIterator<Value>`, `IntoIterator`,
18+
`From<Array> for Value`, `into_value`
1519
- read access: `len`, `is_empty`, `get`, `first`, `last`, `as_slice`, indexing
1620
- mutation: `get_mut`, `push`, `pop`, `clear`, `insert`, `remove`, `truncate`,
1721
`extend_from_slice`, `iter_mut`, `sort`, `sort_by`, `dedup`, `reverse`
1822
- iteration: `iter`, borrowed/mutable/owned `IntoIterator`, serde
1923
- RVM-only resumable traversal: `cursor` and `next`, both compiled only with
20-
`#[cfg(feature = "rvm")]`
24+
`#[cfg(feature = "rvm")]`; the cursor type is `ArrayCursor`, mirroring
25+
`ObjectCursor`.
2126

2227
Iteration follows sequence order. Cursor types support incremental traversal
2328
needed by the RVM iteration state without exposing iterator internals.
@@ -29,6 +34,20 @@ by materializing a contiguous view internally.
2934
`Ord` is hand-written against the sequence iterator rather than derived from the
3035
storage, so future backends compare exactly like today's `Vec<Value>` payload.
3136

37+
## Memory-limit enforcement
38+
39+
Growth-path methods that return `Result` (`push`, `extend_from_slice`,
40+
`insert`, `with_capacity`, `try_from_vec`, `try_from_iter`) consult the
41+
cooperative memory limit and report `Err` when it would be exceeded; the
42+
array is left in a consistent state — any in-progress mutation that would
43+
cross the limit is rolled back before the error is returned.
44+
45+
Infallible trait impls (`Extend<Value>`, `FromIterator<Value>`,
46+
`From<Vec<Value>>`) match the [`Object`](object.md) convention and **do not**
47+
consult the cooperative limit; they exist for ergonomic and migration use and
48+
rely on the allocator to fail on true OOM. Limit-aware callers must use the
49+
explicit fallible methods above.
50+
3251
## Scenarios enabled
3352

3453
- **Inline-small storage** — store short arrays inline and spill to heap only for

src/value/array/iter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ impl<'a> FusedIterator for IterMut<'a> {}
127127
/// of a long-lived state struct (e.g. an RVM iteration frame).
128128
#[cfg(feature = "rvm")]
129129
#[derive(Debug, Clone, Default)]
130-
pub struct Cursor {
130+
pub struct ArrayCursor {
131131
pub(super) next: usize,
132132
}
133133

src/value/array/mod.rs

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ use core::cmp::Ordering;
1111
use core::fmt;
1212
use core::ops;
1313

14-
use anyhow::Result;
14+
use anyhow::{bail, Result};
1515

1616
use crate::value::Value;
1717

1818
#[cfg(feature = "rvm")]
1919
#[allow(unused_imports)] // surface for downstream PRs
20-
pub use iter::Cursor;
20+
pub use iter::ArrayCursor;
2121
#[allow(unused_imports)] // surface for downstream PRs
2222
pub use iter::{IntoIter, Iter, IterMut};
2323

@@ -32,6 +32,21 @@ pub use iter::{IntoIter, Iter, IterMut};
3232
/// - [`Array::iter`] — sequence order; non-resumable.
3333
/// - [`Array::cursor`] / [`Array::next`] — sequence order, resumable. Used by
3434
/// interpreter/RVM when iteration must yield mid-flight.
35+
///
36+
/// # Memory-limit enforcement
37+
///
38+
/// Growth-path methods that take a fallible `Result` return value
39+
/// ([`Array::push`], [`Array::extend_from_slice`], [`Array::insert`],
40+
/// [`Array::with_capacity`], [`Array::try_from_vec`], [`Array::try_from_iter`])
41+
/// consult the configured cooperative memory limit and report
42+
/// `Err` when it is exceeded; the array is left in a consistent state (any
43+
/// in-progress mutation that would have crossed the limit is rolled back).
44+
///
45+
/// Infallible trait impls ([`Extend`], [`FromIterator`], [`From<Vec<Value>>`])
46+
/// match the [`crate::value::Object`] convention and **do not** consult the
47+
/// cooperative limit — they exist for ergonomic / migration use and rely on the
48+
/// allocator to fail on true OOM. Limit-aware callers must use the explicit
49+
/// fallible methods above.
3550
#[derive(Default, Clone, Eq, PartialEq)]
3651
pub struct Array {
3752
inner: Vec<Value>,
@@ -46,12 +61,14 @@ impl Array {
4661

4762
/// Create an empty `Array` with space for at least `capacity` elements.
4863
///
49-
/// Returns `None` if the requested capacity is too large or cannot be
50-
/// reserved by the allocator.
64+
/// Returns `None` if the requested capacity is too large for the
65+
/// allocator, or if reserving it would push memory usage past the
66+
/// configured cooperative limit.
5167
#[inline]
5268
pub fn with_capacity(capacity: usize) -> Option<Self> {
5369
let mut inner = Vec::new();
5470
inner.try_reserve(capacity).ok()?;
71+
crate::utils::limits::check_memory_limit_if_needed().ok()?;
5572
Some(Self { inner })
5673
}
5774

@@ -106,12 +123,18 @@ impl Array {
106123

107124
/// Appends `value` to the end of the array.
108125
///
109-
/// Returns an error if the configured memory limit is exceeded after the
110-
/// growth operation.
126+
/// Returns `Err` if the cooperative memory limit would be exceeded by the
127+
/// appended element; in that case the value is rolled back (popped) before
128+
/// returning, so the array length is unchanged.
111129
#[inline]
112130
pub fn push(&mut self, value: Value) -> Result<()> {
113131
self.inner.push(value);
114-
crate::utils::limits::check_memory_limit_if_needed().map_err(anyhow::Error::new)
132+
if let Err(err) = crate::utils::limits::check_memory_limit_if_needed() {
133+
// Roll back the mutation so the caller observes a consistent state.
134+
self.inner.pop();
135+
return Err(anyhow::Error::new(err));
136+
}
137+
Ok(())
115138
}
116139

117140
#[inline]
@@ -126,15 +149,22 @@ impl Array {
126149

127150
/// Inserts `value` at `index`, shifting existing elements right.
128151
///
129-
/// Returns `Some(())` on success, or `None` if `index > self.len()`
130-
/// (rather than panicking — this is a host-reachable API surface).
152+
/// Returns `Err` if `index > self.len()` (rather than panicking — this is
153+
/// a host-reachable API surface) or if the cooperative memory limit would
154+
/// be exceeded by the appended element; in the limit case the insertion is
155+
/// rolled back so the array length is unchanged.
131156
#[inline]
132-
pub fn insert(&mut self, index: usize, value: Value) -> Option<()> {
133-
if index > self.inner.len() {
134-
return None;
157+
pub fn insert(&mut self, index: usize, value: Value) -> Result<()> {
158+
let len = self.inner.len();
159+
if index > len {
160+
bail!("Array::insert: index {index} out of bounds (len={len})");
135161
}
136162
self.inner.insert(index, value);
137-
Some(())
163+
if let Err(err) = crate::utils::limits::check_memory_limit_if_needed() {
164+
self.inner.remove(index);
165+
return Err(anyhow::Error::new(err));
166+
}
167+
Ok(())
138168
}
139169

140170
/// Removes and returns the element at `index`, shifting subsequent
@@ -175,11 +205,17 @@ impl Array {
175205

176206
/// Extends the array by cloning values from `other`.
177207
///
178-
/// Checks the configured memory limit after each appended element.
179-
#[inline]
208+
/// Checks the cooperative memory limit after each appended element; on
209+
/// failure, all elements appended by this call (including the offending
210+
/// one) are rolled back so the array length is unchanged.
180211
pub fn extend_from_slice(&mut self, other: &[Value]) -> Result<()> {
212+
let original_len = self.inner.len();
181213
for value in other {
182-
self.push(value.clone())?;
214+
self.inner.push(value.clone());
215+
if let Err(err) = crate::utils::limits::check_memory_limit_if_needed() {
216+
self.inner.truncate(original_len);
217+
return Err(anyhow::Error::new(err));
218+
}
183219
}
184220
Ok(())
185221
}
@@ -189,6 +225,27 @@ impl Array {
189225
self.inner.reverse();
190226
}
191227

228+
/// Build an `Array` from an existing `Vec<Value>` with memory-limit
229+
/// enforcement. Returns `Err` if the cooperative limit is exceeded after
230+
/// taking ownership of `values`.
231+
#[inline]
232+
pub fn try_from_vec(values: Vec<Value>) -> Result<Self> {
233+
let array = Self { inner: values };
234+
crate::utils::limits::check_memory_limit_if_needed().map_err(anyhow::Error::new)?;
235+
Ok(array)
236+
}
237+
238+
/// Collect an iterator of `Value`s into an `Array` with memory-limit
239+
/// enforcement per-element. Rolls back any partially-collected state on
240+
/// limit failure (the returned `Err` leaves nothing behind).
241+
pub fn try_from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Result<Self> {
242+
let mut array = Self::new();
243+
for value in iter {
244+
array.push(value)?;
245+
}
246+
Ok(array)
247+
}
248+
192249
/// Wrap into a `Value::Array`.
193250
#[inline]
194251
pub fn into_value(self) -> Value {
@@ -205,13 +262,13 @@ impl Array {
205262
/// current contents at each stored index.
206263
#[cfg(feature = "rvm")]
207264
#[inline]
208-
pub const fn cursor(&self) -> Cursor {
209-
Cursor { next: 0 }
265+
pub const fn cursor(&self) -> ArrayCursor {
266+
ArrayCursor { next: 0 }
210267
}
211268

212269
/// Advance `cursor` and yield the next `(index, value)` pair. O(1).
213270
#[cfg(feature = "rvm")]
214-
pub fn next<'a>(&'a self, cursor: &mut Cursor) -> Option<(usize, &'a Value)> {
271+
pub fn next<'a>(&'a self, cursor: &mut ArrayCursor) -> Option<(usize, &'a Value)> {
215272
let index = cursor.next;
216273
let value = self.inner.get(index)?;
217274
cursor.next = index.saturating_add(1);
@@ -256,25 +313,25 @@ impl ops::Index<usize> for Array {
256313
}
257314
}
258315

259-
fn abort_on_growth_error(result: Result<()>) {
260-
if result.is_err() {
261-
alloc::alloc::handle_alloc_error(core::alloc::Layout::new::<Value>());
262-
}
263-
}
316+
// Bulk-insertion trait impls mirror the [`crate::value::Object`] convention:
317+
// they delegate directly to the inner container and **do not** consult the
318+
// cooperative memory limit. Limit-aware callers must use [`Array::push`],
319+
// [`Array::extend_from_slice`], [`Array::try_from_vec`], or
320+
// [`Array::try_from_iter`].
264321

265322
impl Extend<Value> for Array {
323+
#[inline]
266324
fn extend<I: IntoIterator<Item = Value>>(&mut self, iter: I) {
267-
for value in iter {
268-
abort_on_growth_error(self.push(value));
269-
}
325+
self.inner.extend(iter);
270326
}
271327
}
272328

273329
impl FromIterator<Value> for Array {
330+
#[inline]
274331
fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
275-
let mut array = Self::new();
276-
array.extend(iter);
277-
array
332+
Self {
333+
inner: Vec::from_iter(iter),
334+
}
278335
}
279336
}
280337

src/value/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
clippy::as_conversions
1212
)] // value helpers index paths directly for performance
1313

14-
pub mod array;
14+
mod array;
1515
mod object;
1616

1717
#[cfg(test)]
@@ -22,6 +22,9 @@ pub use array::Array;
2222
#[allow(unused_imports)] // surface for downstream PRs
2323
pub use object::{IntoIter, Iter, IterMut, Object};
2424

25+
#[cfg(feature = "rvm")]
26+
#[allow(unused_imports)] // surface for downstream PRs
27+
pub use array::ArrayCursor;
2528
#[cfg(feature = "rvm")]
2629
#[allow(unused_imports)] // surface for downstream PRs
2730
pub use object::ObjectCursor;

src/value/tests.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ fn array_mutators_and_accessors() {
609609
let mut array = Array::new();
610610
array.push(val(1)).expect("push should fit");
611611
array.push(val(3)).expect("push should fit");
612-
assert_eq!(array.insert(1, val(2)), Some(()));
612+
array.insert(1, val(2)).expect("insert should succeed");
613613

614614
assert_eq!(array.len(), 3);
615615
assert_eq!(array.get(0), Some(&val(1)));
@@ -641,16 +641,18 @@ fn array_out_of_range_access_is_safe() {
641641
assert_eq!(array[2], Value::Undefined);
642642
assert_eq!(array[usize::MAX], Value::Undefined);
643643

644-
// insert past the end returns None instead of panicking.
645-
assert_eq!(array.insert(99, val(0)), None);
644+
// insert past the end errors instead of panicking.
645+
assert!(array.insert(99, val(0)).is_err());
646646
assert_eq!(array.len(), 2);
647647

648648
// remove past the end returns None instead of panicking.
649649
assert_eq!(array.remove(99), None);
650650
assert_eq!(array.len(), 2);
651651

652652
// insert at exactly len is valid (append-like).
653-
assert_eq!(array.insert(2, val(3)), Some(()));
653+
array
654+
.insert(2, val(3))
655+
.expect("insert at len should succeed");
654656
assert_eq!(array.as_slice(), &[val(1), val(2), val(3)]);
655657
}
656658

@@ -774,3 +776,28 @@ fn array_iterators_are_double_ended_exact_and_fused() {
774776
assert_eq!(owned.next_back(), Some(val(3)));
775777
assert_eq!(owned.collect::<Vec<_>>(), vec![val(1), val(2)]);
776778
}
779+
780+
#[test]
781+
fn array_try_from_vec_and_try_from_iter() {
782+
let from_vec = Array::try_from_vec(vec![val(1), val(2), val(3)])
783+
.expect("try_from_vec under no limit succeeds");
784+
assert_eq!(from_vec.as_slice(), &[val(1), val(2), val(3)]);
785+
786+
let from_iter = Array::try_from_iter([val(1), val(2), val(3)])
787+
.expect("try_from_iter under no limit succeeds");
788+
assert_eq!(from_iter, from_vec);
789+
}
790+
791+
#[test]
792+
fn array_insert_out_of_bounds_returns_err() {
793+
let mut array = Array::from_iter([val(1), val(2)]);
794+
let err = array
795+
.insert(99, val(0))
796+
.expect_err("out-of-bounds insert should error");
797+
let msg = alloc::format!("{err}");
798+
assert!(
799+
msg.contains("out of bounds"),
800+
"error should mention out of bounds: {msg}"
801+
);
802+
assert_eq!(array.as_slice(), &[val(1), val(2)]);
803+
}

0 commit comments

Comments
 (0)