Skip to content
This repository has been archived by the owner on Mar 20, 2024. It is now read-only.

Destroy vectors with elements #368

Merged
merged 1 commit into from Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions language/move-native/src/rt.rs
Expand Up @@ -14,8 +14,7 @@ extern "C" fn abort(code: u64) -> ! {

#[export_name = "move_rt_vec_destroy"]
unsafe extern "C" fn vec_destroy(type_ve: &MoveType, v: MoveUntypedVector) {
assert_eq!(0, v.length, "can't destroy vectors with elements yet");
v.destroy_empty(type_ve);
v.destroy(type_ve);
}

#[export_name = "move_rt_vec_empty"]
Expand Down
46 changes: 44 additions & 2 deletions language/move-native/src/structs.rs
Expand Up @@ -8,7 +8,7 @@ use core::slice;
pub unsafe fn walk_fields<'mv>(
info: &'mv StructTypeInfo,
struct_ref: &'mv AnyValue,
) -> impl Iterator<Item = (&'mv MoveType, &'mv AnyValue, &'mv StaticName)> {
) -> impl DoubleEndedIterator<Item = (&'mv MoveType, &'mv AnyValue, &'mv StaticName)> {
let field_len = usize::try_from(info.field_array_len).expect("overflow");
let fields: &'mv [StructFieldInfo] = slice::from_raw_parts(info.field_array_ptr, field_len);

Expand All @@ -24,7 +24,7 @@ pub unsafe fn walk_fields<'mv>(
pub unsafe fn walk_fields_mut<'mv>(
info: &'mv StructTypeInfo,
struct_ref: *mut AnyValue,
) -> impl Iterator<Item = (&'mv MoveType, *mut AnyValue, &'mv StaticName)> {
) -> impl DoubleEndedIterator<Item = (&'mv MoveType, *mut AnyValue, &'mv StaticName)> {
let field_len = usize::try_from(info.field_array_len).expect("overflow");
let fields: &'mv [StructFieldInfo] = slice::from_raw_parts(info.field_array_ptr, field_len);

Expand All @@ -36,6 +36,48 @@ pub unsafe fn walk_fields_mut<'mv>(
})
}

pub unsafe fn destroy(info: &StructTypeInfo, struct_ref: *mut AnyValue) {
// nb: destroying from back to front. Move doesn't
// have side-effecting dtors so drop order probably doesn't matter.
// Safety: This may not be panic-safe if destroying an element fails.
// This module should be compiled with panic=abort.
for (ty, ptr, _name) in walk_fields_mut(info, struct_ref).rev() {
match ty.type_desc {
TypeDesc::Bool
| TypeDesc::U8
| TypeDesc::U16
| TypeDesc::U32
| TypeDesc::U64
| TypeDesc::U128
| TypeDesc::U256
| TypeDesc::Address
| TypeDesc::Signer
| TypeDesc::Reference => { /* nop */ }
TypeDesc::Vector => {
let elt_type = (*ty.type_info).vector.element_type;
let ptr = ptr as *mut MoveUntypedVector;
// Awkward: MoveUntypedVector::destroy takes by-value self,
// which make sense in most cases, but which we don't have here.
// MoveUntypedVector doesn't otherwise need to clonable,
// and cloning it could be error-prone by making ownership unclear,
// so this clone is just open-coded.
let clone = MoveUntypedVector {
ptr: (*ptr).ptr,
capacity: (*ptr).capacity,
length: (*ptr).length,
};
// nb: indirect recursive call, possible stack overflow.
clone.destroy(elt_type);
}
TypeDesc::Struct => {
let struct_type = &(*ty.type_info).struct_;
// nb: recursive call, possible stack overflow.
destroy(struct_type, ptr);
}
}
}
}

pub unsafe fn cmp_eq(type_ve: &MoveType, s1: &AnyValue, s2: &AnyValue) -> bool {
use crate::conv::{borrow_move_value_as_rust_value, BorrowedTypedMoveValue as BTMV};

Expand Down
63 changes: 52 additions & 11 deletions language/move-native/src/vector.rs
Expand Up @@ -203,8 +203,21 @@ impl MoveUntypedVector {
///
/// Unsafe because the provided type must be correct.
pub unsafe fn destroy_empty(self, type_ve: &MoveType) {
assert_eq!(self.length, 0);
self.destroy(type_ve);
}

/// # Safety
///
/// Unsafe because the provided type must be correct.
//
// todo: We should probably just leak the vector instead of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a flag that will help switch between leak vs. drop. that'd be easier to test i guess. maybe as a separate patch if you think this is a good idea.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can do that separately.

// destroying it, since the program will terminate shortly anyway,
// and Move destructors do not have side effects,
// but for now we are writing the destructor "correctly"
// so that we are sure all the cases are covered and understood.
pub unsafe fn destroy(self, type_ve: &MoveType) {
let v = self;
assert_eq!(v.length, 0);
match type_ve.type_desc {
TypeDesc::Bool => drop(v.into_rust_vec::<bool>()),
TypeDesc::U8 => drop(v.into_rust_vec::<u8>()),
Expand All @@ -216,13 +229,17 @@ impl MoveUntypedVector {
TypeDesc::Address => drop(v.into_rust_vec::<MoveAddress>()),
TypeDesc::Signer => drop(v.into_rust_vec::<MoveSigner>()),
TypeDesc::Vector => {
// Safety: need the correct internal pointer alignment to
// deallocate; need the outer vector to be empty to avoid
// dropping the inner vectors. As in `empty`,
// MoveUntypedVector should have the same size/alignment
// regardless of the contained type, so no need to interpret
// the vector type.
drop(v.into_rust_vec::<MoveUntypedVector>())
// Safety: As in `empty`, the MoveUntypedVector element should have the
// same size/alignment regardless of the contained type.
let mut v = v.into_rust_vec::<MoveUntypedVector>();
// nb: destroying from back to front. Move doesn't
// have side-effecting dtors so drop order probably doesn't matter.
while let Some(v_inner) = v.pop() {
let type_inner_elt = (*type_ve.type_info).vector.element_type;
// nb: recursive call, possible stack overflow.
v_inner.destroy(type_inner_elt);
}
drop(v);
}
TypeDesc::Struct => {
// Safety: like in `empty` we want to deallocate here without
Expand All @@ -232,18 +249,42 @@ impl MoveUntypedVector {
// So here we're just going to free the pointer ourselves,
// constructing a correct `Layout` value to pass to the
// allocator.
//
// Note that this function can only be called on empty vecs,
// so we don't need to care about dropping elements.

let size = (*type_ve.type_info).struct_.size;
let size = usize::try_from(size).expect("overflow");
let alignment = (*type_ve.type_info).struct_.alignment;
let alignment = usize::try_from(alignment).expect("overflow");
let capacity = usize::try_from(v.capacity).expect("overflow");
let length = usize::try_from(v.length).expect("overflow");

assert!(size != 0); // can't handle ZSTs

// A reverse iterator over the elements.
// nb: like the vector case above, destroying back to front.
let elt_ptr_rev_iter = {
let start_ptr = v.ptr;
let size = isize::try_from(size).expect("overflow");
let length = isize::try_from(length).expect("overflow");
let end_ptr = start_ptr.offset(size.checked_mul(length).expect("overflow"));
let mut ptr = end_ptr;
core::iter::from_fn(move || {
if ptr > start_ptr {
ptr = ptr.offset(-size);
Some(ptr)
} else {
None
}
})
};

// Safety: This may not be panic-safe if destroying an element fails.
// This module should be compiled with panic=abort.
for elt_ptr in elt_ptr_rev_iter {
let type_inner_elt = &(*type_ve.type_info).struct_;
// nb: indirect recursive call, possible stack overflow.
crate::structs::destroy(type_inner_elt, elt_ptr as *mut AnyValue);
}

if capacity != 0 {
let vec_byte_size = capacity.checked_mul(size).expect("overflow");
let layout = alloc::alloc::Layout::from_size_align(vec_byte_size, alignment)
Expand Down
7 changes: 3 additions & 4 deletions language/move-stdlib/tests/bcs_tests.move
Expand Up @@ -91,14 +91,13 @@ module std::bcs_tests {
}

#[test]
// TODO remove expected_failre. Should pass
#[expected_failure]
fun encode_128() {
bcs::to_bytes(&box127(true));
}

#[test]
#[expected_failure] // VM_MAX_VALUE_DEPTH_REACHED
// fixme (solana) move-native doesn't implement this recursion limit yet
//#[test]
//#[expected_failure] // VM_MAX_VALUE_DEPTH_REACHED
fun encode_129() {
bcs::to_bytes(&Box { x: box127(true) });
}
Expand Down
116 changes: 116 additions & 0 deletions language/tools/move-mv-llvm-compiler/tests/rbpf-tests/vec-destroy.move
@@ -0,0 +1,116 @@
// use-stdlib

// Testing that the runtime can destroy vectors
// with non-zero number of elements.
//
// At time of writing the compiler does not always
// generate vector destructor calls. The particular
// pattern here, with make returning a vector,
// does generate destructor calls.

module 0x2::tests {
use 0x1::vector;

fun make<T>(elt: T) : vector<T> {
let v: vector<T> = vector::empty();
vector::push_back(&mut v, elt);
v
}

fun make2<T>(elt1: T, elt2: T) : vector<T> {
let v: vector<T> = vector::empty();
vector::push_back(&mut v, elt1);
vector::push_back(&mut v, elt2);
v
}

public fun test_bool() {
make(true);
}

public fun test_u8() {
make(1_u8);
}

public fun test_u16() {
make(1_u16);
}

public fun test_u32() {
make(1_u32);
}

public fun test_u64() {
make(1_u64);
}

public fun test_u128() {
make(1_u128);
}

public fun test_u256() {
make(1_u256);
}

public fun test_vec_u8() {
make(make2(1_u256, 2));
}

public fun test_vec_vec_u8() {
make(make(make2(1_u256, 2)));
}

struct S1 has drop {
}

public fun test_struct1() {
make(make2(S1 { }, S1 { }));
}

struct S2 has drop {
f1: vector<u8>,
f2: bool,
f3: vector<S3>,
}

struct S3 has drop {
f1: vector<S1>,
}

public fun test_struct2() {
make(make2(
S2 {
f1: make(1),
f2: true,
f3: make(S3 {
f1: make(S1 { }),
}),
},
S2 {
f1: make(2),
f2: true,
f3: make(S3 {
f1: make(S1 { }),
}),
}
));
}
}

script {
use 0x2::tests;

fun main() {
tests::test_bool();
tests::test_u8();
tests::test_u16();
tests::test_u32();
tests::test_u64();
tests::test_u128();
tests::test_u256();
tests::test_vec_u8();
tests::test_vec_vec_u8();
tests::test_struct1();
tests::test_struct2();
}
}