Skip to content

Commit

Permalink
Merge pull request #20 from OffchainLabs/fixed-arrays
Browse files Browse the repository at this point in the history
Support Fixed Size Solidity Arrays in the SDK
  • Loading branch information
rauljordan authored Sep 6, 2023
2 parents 6354576 + 1398a1b commit 749a709
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 6 deletions.
23 changes: 17 additions & 6 deletions stylus-proc/src/storage/proc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use lazy_static::lazy_static;
use proc_macro2::Ident;
use proc_macro2::{Ident, Literal};
use quote::quote;
use regex::Regex;
use syn::{
Expand Down Expand Up @@ -115,11 +115,22 @@ impl Parse for SolidityTy {
};

while input.peek(Bracket) {
let _content;
let _ = bracketed!(_content in input); // TODO: fixed arrays
let outer = sdk!("StorageVec");
let inner = quote! { #path };
path = syn::parse_str(&format!("{outer}<{inner}>"))?;
let content;
let _ = bracketed!(content in input);

if content.is_empty() {
let outer = sdk!("StorageVec");
let inner = quote! { #path };
path = syn::parse_str(&format!("{outer}<{inner}>"))?;
} else {
let content: Literal = content.parse()?;
let Ok(size) = content.to_string().parse::<usize>() else {
error!(@content, "Array size must be a positive integer");
};
let outer = sdk!("StorageArray");
let inner = quote! { #path };
path = syn::parse_str(&format!("{outer}<{inner}, {size}>"))?;
}
}

Ok(SolidityTy(path))
Expand Down
125 changes: 125 additions & 0 deletions stylus-sdk/src/storage/array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2023, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md

use super::{Erase, StorageGuard, StorageGuardMut, StorageType};
use alloy_primitives::U256;
use std::marker::PhantomData;

/// Accessor for a storage-backed array.
pub struct StorageArray<S: StorageType, const N: usize> {
slot: U256,
marker: PhantomData<S>,
}

impl<S: StorageType, const N: usize> StorageType for StorageArray<S, N> {
type Wraps<'a> = StorageGuard<'a, StorageArray<S, N>> where Self: 'a;
type WrapsMut<'a> = StorageGuardMut<'a, StorageArray<S, N>> where Self: 'a;

const REQUIRED_SLOTS: usize = Self::required_slots();

unsafe fn new(slot: U256, offset: u8) -> Self {
debug_assert!(offset == 0);
Self {
slot,
marker: PhantomData,
}
}

fn load<'s>(self) -> Self::Wraps<'s> {
StorageGuard::new(self)
}

fn load_mut<'s>(self) -> Self::WrapsMut<'s> {
StorageGuardMut::new(self)
}
}

impl<S: StorageType, const N: usize> StorageArray<S, N> {
/// Gets an accessor to the element at a given index, if it exists.
/// Note: the accessor is protected by a [`StorageGuard`], which restricts
/// its lifetime to that of `&self`.
pub fn getter(&self, index: impl TryInto<usize>) -> Option<StorageGuard<S>> {
let store = unsafe { self.accessor(index)? };
Some(StorageGuard::new(store))
}

/// Gets a mutable accessor to the element at a given index, if it exists.
/// Note: the accessor is protected by a [`StorageGuardMut`], which restricts
/// its lifetime to that of `&mut self`.
pub fn setter(&mut self, index: impl TryInto<usize>) -> Option<StorageGuardMut<S>> {
let store = unsafe { self.accessor(index)? };
Some(StorageGuardMut::new(store))
}

/// Gets the underlying accessor to the element at a given index, if it exists.
///
/// # Safety
///
/// Enables aliasing.
unsafe fn accessor(&self, index: impl TryInto<usize>) -> Option<S> {
let index = index.try_into().ok()?;
if index >= N {
return None;
}
let (slot, offset) = self.index_slot(index);
Some(S::new(slot, offset))
}

/// Gets the underlying accessor to the element at a given index, even if out of bounds.
///
/// # Safety
///
/// Enables aliasing. UB if out of bounds.
unsafe fn accessor_unchecked(&self, index: usize) -> S {
let (slot, offset) = self.index_slot(index);
S::new(slot, offset)
}

/// Gets the element at the given index, if it exists.
pub fn get(&self, index: impl TryInto<usize>) -> Option<S::Wraps<'_>> {
let store = unsafe { self.accessor(index)? };
Some(store.load())
}

/// Gets a mutable accessor to the element at a given index, if it exists.
pub fn get_mut(&mut self, index: impl TryInto<usize>) -> Option<S::WrapsMut<'_>> {
let store = unsafe { self.accessor(index)? };
Some(store.load_mut())
}

/// Determines the slot and offset for the element at an index.
fn index_slot(&self, index: usize) -> (U256, u8) {
let width = S::SLOT_BYTES;
let words = S::REQUIRED_SLOTS.max(1);
let density = Self::density();

let slot = self.slot + U256::from(words * index / density);
let offset = 32 - (width * (1 + index % density)) as u8;
(slot, offset)
}

/// Number of elements per slot.
const fn density() -> usize {
32 / S::SLOT_BYTES
}

/// Required slots for the storage array.
const fn required_slots() -> usize {
let reserved = N * S::REQUIRED_SLOTS;
let density = Self::density();
let packed = (N + density - 1) / density; // ceil division for packed items.
if reserved > packed {
return reserved;
}
packed
}
}

impl<S: Erase, const N: usize> Erase for StorageArray<S, N> {
fn erase(&mut self) {
for i in 0..N {
let mut store = unsafe { self.accessor_unchecked(i) };
store.erase()
}
}
}
2 changes: 2 additions & 0 deletions stylus-sdk/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use alloy_primitives::{Address, BlockHash, BlockNumber, FixedBytes, Signed, Uint
use alloy_sol_types::sol_data::{ByteCount, SupportedFixedBytes};
use core::{cell::OnceCell, marker::PhantomData, ops::Deref};

pub use array::StorageArray;
pub use bytes::{StorageBytes, StorageString};
pub use map::StorageMap;
pub use traits::{
Expand All @@ -20,6 +21,7 @@ pub use cache::StorageCache;
#[cfg(not(feature = "storage-cache"))]
pub use eager::EagerStorage;

mod array;
mod bytes;
mod map;
mod traits;
Expand Down

0 comments on commit 749a709

Please sign in to comment.