Skip to content

Commit e19435c

Browse files
committed
feat: add array map
1 parent af746de commit e19435c

7 files changed

Lines changed: 407 additions & 1 deletion

File tree

.github/workflows/rust.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ jobs:
4242
- name: program-libs-fast
4343
packages:
4444
aligned-sized light-hasher light-compressed-account light-account-checks \
45-
light-verifier light-merkle-tree-metadata light-zero-copy light-hash-set light-concurrent-merkle-tree
45+
light-verifier light-merkle-tree-metadata light-zero-copy light-hash-set light-concurrent-merkle-tree \
46+
light-array-map
4647
test_cmd: |
4748
cargo test -p light-macros
4849
cargo test -p aligned-sized
50+
cargo test -p light-array-map --all-features
4951
cargo test -p light-hasher --all-features
5052
cargo test -p light-compressed-account --all-features
5153
cargo test -p light-compressed-account --features new-unique,poseidon

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
members = [
33
"program-libs/account-checks",
4+
"program-libs/array-map",
45
"program-libs/compressed-account",
56
"program-libs/aligned-sized",
67
"program-libs/batched-merkle-tree",
@@ -200,11 +201,13 @@ light-bounded-vec = { version = "2.0.0" }
200201
light-poseidon = { version = "0.3.0" }
201202
light-test-utils = { path = "program-tests/utils", version = "1.2.1" }
202203
light-indexed-array = { path = "program-libs/indexed-array", version = "0.2.0" }
204+
light-array-map = { path = "program-libs/array-map", version = "0.1.0" }
203205
light-program-profiler = { version = "0.1.0" }
204206
create-address-program-test = { path = "program-tests/create-address-test-program", version = "1.0.0" }
205207
groth16-solana = { version = "0.2.0" }
206208
bytemuck = { version = "1.19.0" }
207209
arrayvec = "0.7"
210+
tinyvec = "1.10.0"
208211

209212
# Math and crypto
210213
num-bigint = "0.4.6"

program-libs/array-map/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "light-array-map"
3+
version = "0.1.0"
4+
description = "Generic array-backed map with O(n) lookup for small collections"
5+
repository = "https://github.com/Lightprotocol/light-protocol"
6+
license = "Apache-2.0"
7+
edition = "2021"
8+
9+
10+
[dependencies]
11+
tinyvec = { workspace = true }

program-libs/array-map/src/lib.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#![no_std]
2+
3+
use core::ptr::read_unaligned;
4+
5+
use tinyvec::ArrayVec;
6+
7+
/// A generic tinyvec::ArrayVec backed map with O(n) lookup.
8+
/// Maintains insertion order and tracks the last updated entry index.
9+
pub struct ArrayMap<K, V, const N: usize>
10+
where
11+
K: PartialEq + Default,
12+
V: Default,
13+
{
14+
entries: ArrayVec<[(K, V); N]>,
15+
last_updated_index: Option<usize>,
16+
}
17+
18+
impl<K, V, const N: usize> ArrayMap<K, V, N>
19+
where
20+
K: PartialEq + Default,
21+
V: Default,
22+
{
23+
pub fn new() -> Self {
24+
Self {
25+
entries: ArrayVec::new(),
26+
last_updated_index: None,
27+
}
28+
}
29+
30+
pub fn len(&self) -> usize {
31+
self.entries.len()
32+
}
33+
34+
pub fn is_empty(&self) -> bool {
35+
self.entries.is_empty()
36+
}
37+
38+
pub fn last_updated_index(&self) -> Option<usize> {
39+
self.last_updated_index
40+
}
41+
42+
pub fn get(&self, index: usize) -> Option<&(K, V)> {
43+
self.entries.get(index)
44+
}
45+
46+
pub fn get_mut(&mut self, index: usize) -> Option<&mut (K, V)> {
47+
self.entries.get_mut(index)
48+
}
49+
50+
pub fn get_u8(&self, index: u8) -> Option<&(K, V)> {
51+
self.get(index as usize)
52+
}
53+
54+
pub fn get_mut_u8(&mut self, index: u8) -> Option<&mut (K, V)> {
55+
self.get_mut(index as usize)
56+
}
57+
58+
pub fn get_by_key(&self, key: &K) -> Option<&V> {
59+
self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
60+
}
61+
62+
pub fn get_mut_by_key(&mut self, key: &K) -> Option<&mut V> {
63+
self.entries
64+
.iter_mut()
65+
.find(|(k, _)| k == key)
66+
.map(|(_, v)| v)
67+
}
68+
69+
pub fn find(&self, key: &K) -> Option<(usize, &(K, V))> {
70+
self.entries.iter().enumerate().find(|(_, (k, _))| k == key)
71+
}
72+
73+
pub fn find_mut(&mut self, key: &K) -> Option<(usize, &mut (K, V))> {
74+
self.entries
75+
.iter_mut()
76+
.enumerate()
77+
.find(|(_, (k, _))| k == key)
78+
}
79+
80+
pub fn find_index(&self, key: &K) -> Option<usize> {
81+
self.find(key).map(|(idx, _)| idx)
82+
}
83+
84+
pub fn set_last_updated_index<E>(&mut self, index: usize) -> Result<(), E>
85+
where
86+
E: From<ArrayMapError>,
87+
{
88+
if index < self.entries.len() {
89+
self.last_updated_index = Some(index);
90+
Ok(())
91+
} else {
92+
Err(ArrayMapError::IndexOutOfBounds.into())
93+
}
94+
}
95+
96+
pub fn insert<E>(&mut self, key: K, value: V, error: E) -> Result<usize, E> {
97+
let new_idx = self.entries.len();
98+
// tinyvec's try_push returns Some(item) on failure, None on success
99+
if self.entries.try_push((key, value)).is_some() {
100+
return Err(error);
101+
}
102+
self.last_updated_index = Some(new_idx);
103+
Ok(new_idx)
104+
}
105+
}
106+
107+
impl<K, V, const N: usize> Default for ArrayMap<K, V, N>
108+
where
109+
K: PartialEq + Default,
110+
V: Default,
111+
{
112+
fn default() -> Self {
113+
Self::new()
114+
}
115+
}
116+
117+
// Optimized [u8; 32] key methods (4x u64 comparison instead of 32x u8).
118+
impl<V, const N: usize> ArrayMap<[u8; 32], V, N>
119+
where
120+
V: Default,
121+
{
122+
pub fn get_by_pubkey(&self, key: &[u8; 32]) -> Option<&V> {
123+
self.entries
124+
.iter()
125+
.find(|(k, _)| pubkey_eq(k, key))
126+
.map(|(_, v)| v)
127+
}
128+
129+
pub fn get_mut_by_pubkey(&mut self, key: &[u8; 32]) -> Option<&mut V> {
130+
self.entries
131+
.iter_mut()
132+
.find(|(k, _)| pubkey_eq(k, key))
133+
.map(|(_, v)| v)
134+
}
135+
136+
pub fn find_by_pubkey(&self, key: &[u8; 32]) -> Option<(usize, &([u8; 32], V))> {
137+
self.entries
138+
.iter()
139+
.enumerate()
140+
.find(|(_, (k, _))| pubkey_eq(k, key))
141+
}
142+
143+
pub fn find_mut_by_pubkey(&mut self, key: &[u8; 32]) -> Option<(usize, &mut ([u8; 32], V))> {
144+
self.entries
145+
.iter_mut()
146+
.enumerate()
147+
.find(|(_, (k, _))| pubkey_eq(k, key))
148+
}
149+
150+
pub fn find_pubkey_index(&self, key: &[u8; 32]) -> Option<usize> {
151+
self.find_by_pubkey(key).map(|(idx, _)| idx)
152+
}
153+
}
154+
155+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156+
pub enum ArrayMapError {
157+
CapacityExceeded,
158+
IndexOutOfBounds,
159+
}
160+
161+
impl core::fmt::Display for ArrayMapError {
162+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
163+
match self {
164+
ArrayMapError::CapacityExceeded => write!(f, "ArrayMap capacity exceeded"),
165+
ArrayMapError::IndexOutOfBounds => write!(f, "ArrayMap index out of bounds"),
166+
}
167+
}
168+
}
169+
170+
#[inline(always)]
171+
pub const fn pubkey_eq(p1: &[u8; 32], p2: &[u8; 32]) -> bool {
172+
let p1_ptr = p1.as_ptr() as *const u64;
173+
let p2_ptr = p2.as_ptr() as *const u64;
174+
175+
unsafe {
176+
read_unaligned(p1_ptr) == read_unaligned(p2_ptr)
177+
&& read_unaligned(p1_ptr.add(1)) == read_unaligned(p2_ptr.add(1))
178+
&& read_unaligned(p1_ptr.add(2)) == read_unaligned(p2_ptr.add(2))
179+
&& read_unaligned(p1_ptr.add(3)) == read_unaligned(p2_ptr.add(3))
180+
}
181+
}

0 commit comments

Comments
 (0)