diff --git a/stylus-proc/src/storage/proc.rs b/stylus-proc/src/storage/proc.rs index 60bcc8d1..43685b77 100644 --- a/stylus-proc/src/storage/proc.rs +++ b/stylus-proc/src/storage/proc.rs @@ -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::{ @@ -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::() 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)) diff --git a/stylus-sdk/src/storage/array.rs b/stylus-sdk/src/storage/array.rs new file mode 100644 index 00000000..074da031 --- /dev/null +++ b/stylus-sdk/src/storage/array.rs @@ -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 { + slot: U256, + marker: PhantomData, +} + +impl StorageType for StorageArray { + type Wraps<'a> = StorageGuard<'a, StorageArray> where Self: 'a; + type WrapsMut<'a> = StorageGuardMut<'a, StorageArray> 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 StorageArray { + /// 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) -> Option> { + 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) -> Option> { + 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) -> Option { + 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) -> Option> { + 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) -> Option> { + 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 Erase for StorageArray { + fn erase(&mut self) { + for i in 0..N { + let mut store = unsafe { self.accessor_unchecked(i) }; + store.erase() + } + } +} diff --git a/stylus-sdk/src/storage/mod.rs b/stylus-sdk/src/storage/mod.rs index c0230d9b..cbec9be0 100644 --- a/stylus-sdk/src/storage/mod.rs +++ b/stylus-sdk/src/storage/mod.rs @@ -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::{ @@ -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;