Skip to content

Commit e013ce4

Browse files
Jeremy Braunfacebook-github-bot
Jeremy Braun
authored andcommittedJan 30, 2025
Add ThinBoxSliceFrozenValue
Summary: Semantically a `Box<[FrozenValueTyped]>` that is better optimized than even `ThinBoxSlice` in the length one case. Explained in the diff. Implementing this in a reasonable way required making a copy of the `ThinBoxSlice` impl. With more compromises I could have reused the code, but that would have lead to weird non-local invariants, so I think this is a much lesser evil. Reviewed By: JakobDegen Differential Revision: D66831083 fbshipit-source-id: 3b218a6ba5f45751ae937f9464d5b15f155b438d
1 parent 3cc54e8 commit e013ce4

File tree

5 files changed

+557
-0
lines changed

5 files changed

+557
-0
lines changed
 

‎starlark/src/values.rs

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub use crate::values::layout::value::ValueLike;
7676
pub use crate::values::layout::value_lifetimeless::ValueLifetimeless;
7777
pub use crate::values::owned::OwnedFrozenValue;
7878
pub use crate::values::owned::OwnedFrozenValueTyped;
79+
pub use crate::values::thin_box_slice_frozen_value::packed_impl::ThinBoxSliceFrozenValue;
7980
pub use crate::values::trace::Trace;
8081
pub use crate::values::traits::ComplexValue;
8182
pub use crate::values::traits::StarlarkValue;
@@ -124,6 +125,7 @@ pub(crate) mod owned_frozen_ref;
124125
pub(crate) mod recursive_repr_or_json_guard;
125126
mod stack_guard;
126127
pub(crate) mod starlark_type_id;
128+
pub(crate) mod thin_box_slice_frozen_value;
127129
mod trace;
128130
pub(crate) mod traits;
129131
pub mod type_repr;

‎starlark/src/values/layout/typed.rs

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub struct ValueTyped<'v, T: StarlarkValue<'v>>(Value<'v>, marker::PhantomData<&
7777
/// [`FrozenValue`] wrapper which asserts contained value is of type `<T>`.
7878
#[derive(Copy_, Clone_, Dupe_, ProvidesStaticType, Allocative)]
7979
#[allocative(skip)] // Heap owns the value.
80+
#[repr(transparent)]
8081
pub struct FrozenValueTyped<'v, T: StarlarkValue<'v>>(FrozenValue, marker::PhantomData<&'v T>);
8182

8283
unsafe impl<'v, T: StarlarkValue<'v>> Coerce<ValueTyped<'v, T>> for ValueTyped<'v, T> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2018 The Starlark in Rust Authors.
3+
* Copyright (c) Facebook, Inc. and its affiliates.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
pub(crate) mod packed_impl;
19+
mod thin_box;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 2018 The Starlark in Rust Authors.
3+
* Copyright (c) Facebook, Inc. and its affiliates.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
use std::marker::PhantomData;
19+
use std::mem;
20+
use std::ops::Deref;
21+
use std::ptr::NonNull;
22+
23+
use either::Either;
24+
25+
use crate::values::thin_box_slice_frozen_value::thin_box::AllocatedThinBoxSlice;
26+
use crate::values::FrozenValue;
27+
28+
/// Wrapper to handle the packing and most of the unsafety.
29+
///
30+
/// The representation is as follows:
31+
/// - In the case of a length 1 slice, the `FrozenValue` is stored in the `NonNull` pointer.
32+
/// - In all other cases, the `NonNull` is a `AllocatedThinBoxSlice<FrozenValue>` with the bottom
33+
/// bit set to 1.
34+
struct PackedImpl(NonNull<()>);
35+
36+
impl PackedImpl {
37+
const fn new_allocated(allocated: AllocatedThinBoxSlice<FrozenValue>) -> Self {
38+
let allocated = unsafe { allocated.into_inner().byte_add(1) };
39+
Self(allocated.cast::<()>())
40+
}
41+
42+
fn new(iter: impl IntoIterator<Item = FrozenValue>) -> Self {
43+
let mut iter = iter.into_iter();
44+
let Some(first) = iter.next() else {
45+
return Self::new_allocated(AllocatedThinBoxSlice::empty());
46+
};
47+
let Some(second) = iter.next() else {
48+
return Self(unsafe { mem::transmute::<FrozenValue, NonNull<()>>(first) });
49+
};
50+
Self::new_allocated(AllocatedThinBoxSlice::from_iter(
51+
[first, second].into_iter().chain(iter),
52+
))
53+
}
54+
55+
fn unpack(&self) -> Either<&FrozenValue, AllocatedThinBoxSlice<FrozenValue>> {
56+
let ptr = self.0.as_ptr();
57+
if (ptr as usize) & 1 == 1 {
58+
let allocated = (ptr as usize & !1) as *mut FrozenValue;
59+
let allocated = unsafe {
60+
AllocatedThinBoxSlice::<FrozenValue>::from_inner(
61+
NonNull::<FrozenValue>::new_unchecked(allocated),
62+
)
63+
};
64+
Either::Right(allocated)
65+
} else {
66+
let val = unsafe { &*(self as *const PackedImpl as *const FrozenValue) };
67+
Either::Left(val)
68+
}
69+
}
70+
71+
fn as_slice(&self) -> &[FrozenValue] {
72+
match self.unpack() {
73+
Either::Left(val) => std::slice::from_ref(val),
74+
Either::Right(allocated) => {
75+
let slice: &[FrozenValue] = &allocated;
76+
let slice: &[FrozenValue] = unsafe { mem::transmute(slice) };
77+
slice
78+
}
79+
}
80+
}
81+
}
82+
83+
impl Drop for PackedImpl {
84+
fn drop(&mut self) {
85+
match self.unpack() {
86+
Either::Left(_) => {}
87+
Either::Right(allocated) => allocated.run_drop(),
88+
}
89+
}
90+
}
91+
92+
impl allocative::Allocative for PackedImpl {
93+
fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) {
94+
// Intentionally don't `enter_self_sized()`, and instead just report the
95+
// `ThinBoxSliceFrozenValue` itself
96+
match self.unpack() {
97+
Either::Left(value) => {
98+
visitor.visit_simple(allocative::Key::new("inline"), std::mem::size_of_val(value));
99+
}
100+
Either::Right(allocated) => {
101+
allocative::Allocative::visit(&allocated, visitor);
102+
}
103+
}
104+
}
105+
}
106+
107+
unsafe impl Send for PackedImpl where FrozenValue: Send {}
108+
unsafe impl Sync for PackedImpl where FrozenValue: Sync {}
109+
110+
/// Optimized version of a `Box<[FrozenValue]>`.
111+
///
112+
/// Specifically, this type uses bit packing and other tricks so that it is only
113+
/// 8 bytes in size, while being allocation free for lengths zero and one. It
114+
/// depends on the lower bit of a FrozenPointer always being unset.
115+
pub struct ThinBoxSliceFrozenValue<'v>(PackedImpl, PhantomData<&'v ()>);
116+
117+
impl<'v> ThinBoxSliceFrozenValue<'v> {
118+
/// Produces an empty list
119+
pub const fn empty() -> Self {
120+
Self(
121+
PackedImpl::new_allocated(AllocatedThinBoxSlice::empty()),
122+
PhantomData,
123+
)
124+
}
125+
}
126+
127+
impl<'v> Deref for ThinBoxSliceFrozenValue<'v> {
128+
type Target = [FrozenValue];
129+
130+
#[inline]
131+
fn deref<'a>(&'a self) -> &'a Self::Target {
132+
self.0.as_slice()
133+
}
134+
}
135+
136+
impl<'v> FromIterator<FrozenValue> for ThinBoxSliceFrozenValue<'v> {
137+
fn from_iter<I: IntoIterator<Item = FrozenValue>>(iter: I) -> Self {
138+
Self(PackedImpl::new(iter), PhantomData)
139+
}
140+
}
141+
142+
impl<'v> allocative::Allocative for ThinBoxSliceFrozenValue<'v> {
143+
fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) {
144+
let mut visitor = visitor.enter_self_sized::<Self>();
145+
allocative::Allocative::visit(&self.0, &mut visitor);
146+
visitor.exit();
147+
}
148+
}
149+
150+
impl<'v> Default for ThinBoxSliceFrozenValue<'v> {
151+
#[inline]
152+
fn default() -> Self {
153+
ThinBoxSliceFrozenValue::from_iter(std::iter::empty())
154+
}
155+
}
156+
157+
impl<'v> std::fmt::Debug for ThinBoxSliceFrozenValue<'v> {
158+
#[inline]
159+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160+
<[_] as std::fmt::Debug>::fmt(&self, f)
161+
}
162+
}
163+
164+
impl<'v> PartialEq for ThinBoxSliceFrozenValue<'v> {
165+
#[inline]
166+
fn eq(&self, other: &Self) -> bool {
167+
<[_] as PartialEq>::eq(&**self, &**other)
168+
}
169+
}
170+
171+
impl<'v> Eq for ThinBoxSliceFrozenValue<'v> {}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::ThinBoxSliceFrozenValue;
176+
use crate::values::int::inline_int::InlineInt;
177+
use crate::values::FrozenHeap;
178+
use crate::values::FrozenValue;
179+
180+
fn across_lengths(a: [FrozenValue; 16]) {
181+
for len in 0..=16 {
182+
let val = ThinBoxSliceFrozenValue::from_iter(a.into_iter().take(len));
183+
assert_eq!(val.len(), len);
184+
assert_eq!(&*val, &a[..len]);
185+
}
186+
}
187+
188+
#[test]
189+
fn test_strings() {
190+
let h = FrozenHeap::new();
191+
let s: [_; 16] = ["", "abc", "def", "ghijkl"].repeat(4).try_into().unwrap();
192+
let s = s.map(|s| h.alloc_str(s).to_frozen_value());
193+
across_lengths(s);
194+
}
195+
196+
#[test]
197+
fn test_ints() {
198+
let i: [_; 16] = [0, 1, 2, 3, 4, 5, 1000, 1 << 20]
199+
.repeat(2)
200+
.try_into()
201+
.unwrap();
202+
let i = i.map(|i| FrozenValue::new_int(InlineInt::testing_new(i)));
203+
across_lengths(i);
204+
}
205+
206+
#[test]
207+
fn test_mixed_types() {
208+
let a: [_; 16] = [
209+
FrozenValue::new_none(),
210+
FrozenValue::new_int(InlineInt::testing_new(0)),
211+
FrozenValue::new_empty_list(),
212+
FrozenValue::new_bool(true),
213+
]
214+
.repeat(4)
215+
.try_into()
216+
.unwrap();
217+
218+
across_lengths(a);
219+
}
220+
221+
#[test]
222+
fn test_default() {
223+
let val = ThinBoxSliceFrozenValue::default();
224+
assert_eq!(val.len(), 0);
225+
}
226+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
* Copyright 2018 The Starlark in Rust Authors.
3+
* Copyright (c) Facebook, Inc. and its affiliates.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
//! This type is a copy-paste of `buck2_util::thin_box::ThinBoxSlice`, with some mild adjustments.
19+
//!
20+
//! Specifically:
21+
//! 1. This type guarantees that it's always a pointer with the bottom bit zero.
22+
//! 2. This type is not implicitly dropped - `run_drop` must be called explicitly.
23+
24+
use std::alloc;
25+
use std::alloc::Layout;
26+
use std::fmt::Debug;
27+
use std::hash::Hash;
28+
use std::hash::Hasher;
29+
use std::mem;
30+
use std::mem::MaybeUninit;
31+
use std::ops::Deref;
32+
use std::ops::DerefMut;
33+
use std::ptr;
34+
use std::ptr::NonNull;
35+
use std::slice;
36+
37+
use allocative::Allocative;
38+
39+
#[repr(C)]
40+
struct ThinBoxSliceLayout<T> {
41+
len: usize,
42+
data: [T; 0],
43+
}
44+
45+
impl<T> ThinBoxSliceLayout<T> {
46+
fn offset_of_data() -> usize {
47+
mem::offset_of!(ThinBoxSliceLayout::<T>, data)
48+
}
49+
}
50+
51+
/// `Box<[T]>` but thin pointer.
52+
///
53+
/// Statically allocated for empty slice.
54+
// We don't really need `'static` here, but we hit type checker limitations.
55+
pub(super) struct AllocatedThinBoxSlice<T: 'static> {
56+
/// Pointer to the first element, `ThinBoxSliceLayout.data`.
57+
ptr: NonNull<T>,
58+
}
59+
60+
unsafe impl<T: Sync> Sync for AllocatedThinBoxSlice<T> {}
61+
unsafe impl<T: Send> Send for AllocatedThinBoxSlice<T> {}
62+
63+
impl<T: 'static> AllocatedThinBoxSlice<T> {
64+
#[inline]
65+
pub(super) const fn empty() -> AllocatedThinBoxSlice<T> {
66+
const fn instance<T>() -> &'static ThinBoxSliceLayout<T> {
67+
assert!(mem::size_of::<ThinBoxSliceLayout<T>>() <= mem::size_of::<u128>());
68+
assert!(mem::align_of::<ThinBoxSliceLayout<T>>() <= mem::align_of::<u128>());
69+
// Instead of just statically allocating a `ThinBoxSliceLayout<T>` we allocate a
70+
// `0_u128`. The reason for this is a rustc bug around const UB checks that otherwise
71+
// incorrectly fires: https://github.com/rust-lang/rust/issues/133523
72+
//
73+
// SAFETY: We just checked that the layout is small enough to fit in a u128.
74+
unsafe { &*ptr::from_ref(&0u128).cast::<ThinBoxSliceLayout<T>>() }
75+
}
76+
77+
unsafe {
78+
AllocatedThinBoxSlice {
79+
ptr: NonNull::new_unchecked(instance::<T>().data.as_ptr() as *mut T),
80+
}
81+
}
82+
}
83+
84+
/// Allocation layout for a slice of length `len`.
85+
#[inline]
86+
fn layout_for_len(len: usize) -> Layout {
87+
let (layout, _offset_of_data) = Layout::new::<ThinBoxSliceLayout<T>>()
88+
.extend(Layout::array::<T>(len).unwrap())
89+
.unwrap();
90+
layout
91+
}
92+
93+
/// Length of the slice.
94+
// Not called `len` to avoid overload with `Deref::len`.
95+
#[inline]
96+
fn read_len(&self) -> usize {
97+
unsafe {
98+
(*self
99+
.ptr
100+
.as_ptr()
101+
.cast::<u8>()
102+
.sub(ThinBoxSliceLayout::<T>::offset_of_data())
103+
.cast::<ThinBoxSliceLayout<T>>())
104+
.len
105+
}
106+
}
107+
108+
/// Allocate uninitialized memory for a slice of length `len`.
109+
#[inline]
110+
pub(super) fn new_uninit(len: usize) -> AllocatedThinBoxSlice<MaybeUninit<T>> {
111+
if len == 0 {
112+
AllocatedThinBoxSlice::empty()
113+
} else {
114+
let layout = Self::layout_for_len(len);
115+
unsafe {
116+
let alloc = alloc::alloc(layout);
117+
if alloc.is_null() {
118+
alloc::handle_alloc_error(layout);
119+
}
120+
ptr::write(alloc as *mut usize, len);
121+
let ptr = alloc.add(mem::size_of::<usize>()) as *mut MaybeUninit<T>;
122+
let ptr = NonNull::new_unchecked(ptr);
123+
AllocatedThinBoxSlice { ptr }
124+
}
125+
}
126+
}
127+
128+
pub const unsafe fn into_inner(self) -> NonNull<T> {
129+
self.ptr
130+
}
131+
132+
pub const unsafe fn from_inner(ptr: NonNull<T>) -> Self {
133+
Self { ptr }
134+
}
135+
}
136+
137+
impl<T: 'static> Deref for AllocatedThinBoxSlice<T> {
138+
type Target = [T];
139+
140+
#[inline]
141+
fn deref(&self) -> &Self::Target {
142+
unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.read_len()) }
143+
}
144+
}
145+
146+
impl<T: 'static> DerefMut for AllocatedThinBoxSlice<T> {
147+
#[inline]
148+
fn deref_mut(&mut self) -> &mut Self::Target {
149+
unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.read_len()) }
150+
}
151+
}
152+
153+
impl<T> AllocatedThinBoxSlice<MaybeUninit<T>> {
154+
#[inline]
155+
unsafe fn assume_init(self) -> AllocatedThinBoxSlice<T> {
156+
AllocatedThinBoxSlice {
157+
ptr: self.ptr.cast::<T>(),
158+
}
159+
}
160+
}
161+
162+
impl<T: 'static> AllocatedThinBoxSlice<T> {
163+
#[inline]
164+
pub(super) fn run_drop(self) {
165+
unsafe {
166+
let len = self.read_len();
167+
if len != 0 {
168+
let slice = ptr::slice_from_raw_parts_mut(self.ptr.as_ptr(), len);
169+
ptr::drop_in_place(slice);
170+
let alloc = self.ptr.cast::<usize>().as_ptr().sub(1);
171+
alloc::dealloc(alloc as *mut u8, Self::layout_for_len(len));
172+
}
173+
}
174+
}
175+
}
176+
177+
impl<T: 'static> Default for AllocatedThinBoxSlice<T> {
178+
#[inline]
179+
fn default() -> Self {
180+
AllocatedThinBoxSlice::empty()
181+
}
182+
}
183+
184+
impl<T: Debug> Debug for AllocatedThinBoxSlice<T> {
185+
#[inline]
186+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187+
<[T] as Debug>::fmt(&**self, f)
188+
}
189+
}
190+
191+
impl<T: PartialEq> PartialEq for AllocatedThinBoxSlice<T> {
192+
#[inline]
193+
fn eq(&self, other: &Self) -> bool {
194+
<[T] as PartialEq>::eq(&**self, &**other)
195+
}
196+
}
197+
198+
impl<T: Eq> Eq for AllocatedThinBoxSlice<T> {}
199+
200+
impl<T: PartialOrd> PartialOrd for AllocatedThinBoxSlice<T> {
201+
#[inline]
202+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
203+
<[T] as PartialOrd>::partial_cmp(&**self, &**other)
204+
}
205+
}
206+
207+
impl<T: Hash> Hash for AllocatedThinBoxSlice<T> {
208+
#[inline]
209+
fn hash<H: Hasher>(&self, state: &mut H) {
210+
<[T] as Hash>::hash(&**self, state)
211+
}
212+
}
213+
214+
impl<T> FromIterator<T> for AllocatedThinBoxSlice<T> {
215+
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
216+
let iter = iter.into_iter();
217+
let (lower, upper) = iter.size_hint();
218+
if Some(lower) == upper {
219+
let mut thin = AllocatedThinBoxSlice::<T>::new_uninit(lower);
220+
let mut i = 0;
221+
for item in iter {
222+
assert!(i < lower, "iterator produced more than promised");
223+
MaybeUninit::write(&mut thin[i], item);
224+
i += 1;
225+
}
226+
assert_eq!(i, lower, "iterator produced less than promised");
227+
unsafe { thin.assume_init() }
228+
} else {
229+
// TODO(nga): we can collect into partially initialized `ThinBoxSlice`
230+
// to get a chance of avoiding last reallocation.
231+
let vec = Vec::from_iter(iter);
232+
Self::from_iter(vec)
233+
}
234+
}
235+
}
236+
237+
impl<T: Allocative> Allocative for AllocatedThinBoxSlice<T> {
238+
fn visit<'a, 'b: 'a>(&self, visitor: &'a mut allocative::Visitor<'b>) {
239+
let mut visitor = visitor.enter_self_sized::<Self>();
240+
{
241+
let ptr_key = allocative::Key::new("ptr");
242+
if self.len() == 0 {
243+
// Statically allocated data, so just report the pointer itself
244+
visitor.visit_simple(ptr_key, mem::size_of_val(&self.ptr));
245+
} else {
246+
let mut visitor =
247+
visitor.enter_unique(allocative::Key::new("ptr"), mem::size_of_val(&self.ptr));
248+
{
249+
let mut visitor = visitor.enter(
250+
allocative::Key::new("alloc"),
251+
Self::layout_for_len(self.len()).size(),
252+
);
253+
visitor.visit_simple(allocative::Key::new("len"), mem::size_of::<usize>());
254+
{
255+
let mut visitor = visitor
256+
.enter(allocative::Key::new("data"), mem::size_of_val::<[_]>(self));
257+
visitor.visit_slice::<T>(self);
258+
visitor.exit();
259+
}
260+
visitor.exit();
261+
}
262+
visitor.exit();
263+
}
264+
}
265+
visitor.exit();
266+
}
267+
}
268+
269+
#[cfg(test)]
270+
mod tests {
271+
use super::AllocatedThinBoxSlice;
272+
273+
#[test]
274+
fn test_empty() {
275+
let thin = AllocatedThinBoxSlice::<String>::empty();
276+
assert_eq!(0, thin.len());
277+
thin.run_drop();
278+
}
279+
280+
#[test]
281+
fn test_from_iter_sized() {
282+
let thin =
283+
AllocatedThinBoxSlice::from_iter(["a".to_owned(), "bb".to_owned(), "ccc".to_owned()]);
284+
assert_eq!(["a".to_owned(), "bb".to_owned(), "ccc".to_owned()], *thin);
285+
thin.run_drop();
286+
}
287+
288+
#[test]
289+
fn test_from_iter_unknown_size() {
290+
let thin = AllocatedThinBoxSlice::from_iter(
291+
["a".to_owned(), "b".to_owned(), "c".to_owned()]
292+
.into_iter()
293+
.filter(|_| true),
294+
);
295+
assert_eq!(["a".to_owned(), "b".to_owned(), "c".to_owned()], *thin);
296+
thin.run_drop();
297+
}
298+
299+
/// If there are obvious memory violations, this test will catch them.
300+
#[test]
301+
fn test_stress() {
302+
for i in 0..1000 {
303+
let thin = AllocatedThinBoxSlice::from_iter((0..i).map(|j| j.to_string()));
304+
assert_eq!(i, thin.len());
305+
assert_eq!((0..i).map(|j| j.to_string()).collect::<Vec<_>>(), *thin);
306+
thin.run_drop();
307+
}
308+
}
309+
}

0 commit comments

Comments
 (0)
Please sign in to comment.