Skip to content

Commit 4e5778b

Browse files
authored
feat: initial implementation (#1)
Release-as: 0.1.0
1 parent bea6304 commit 4e5778b

File tree

4 files changed

+687
-23
lines changed

4 files changed

+687
-23
lines changed

Nargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[package]
2-
name = "noir_library"
2+
name = "sparse_array"
33
type = "lib"
44
authors = [""]
55
compiler_version = ">=0.34.0"
66

77
[dependencies]
8+
sort = {tag = "v0.1.0", git = "https://github.com/noir-lang/noir_sort"}

README.md

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
1-
# noir-library-starter
1+
# sparse_array
22

3-
This repository is a template used by the noir-lang org when creating internally maintained libraries.
3+
Noir library that implements efficient sparse arrays, both constant (SparseArray) and mutable (MutSparseArray)
44

5-
This provides out of the box:
6-
7-
- A simple CI setup to test and format the library
8-
- A canary flagging up compilation failures on nightly releases.
9-
- A [release-please](https://github.com/googleapis/release-please) setup to ease creating releases for the library.
10-
11-
Feel free to use this template as a starting point to create your own Noir libraries.
12-
13-
---
14-
15-
# LIBRARY_NAME
16-
17-
Add a brief description of the library
185

196
## Benchmarks
207

@@ -27,11 +14,40 @@ In your _Nargo.toml_ file, add the version of this library you would like to ins
2714

2815
```
2916
[dependencies]
30-
LIBRARY = { tag = "v0.1.0", git = "https://github.com/noir-lang/LIBRARY_NAME" }
17+
sparse_array = { tag = "v0.1.0", git = "https://github.com/noir-lang/sparse_array" }
3118
```
3219

33-
## `library`
20+
## `sparse_array`
3421

3522
### Usage
3623

37-
`PLACEHOLDER`
24+
```rust
25+
use dep::sparse_array::{SparseArray, MutSparseArray}
26+
27+
// a sparse array of size 10,000 with 10 nonzero values
28+
fn example_sparse_array(nonzero_indices: [Field; 10], nonzero_values: [Field; 10]) {
29+
let sparse_array_size = 10000;
30+
let array: SparseArray<10, Field> = SparseArray::create(nonzero_indices, nonzero_values, sparse_array_size);
31+
32+
assert(array.get(999) == 12345);
33+
}
34+
35+
// a mutable sparse array that can contain up to 10 nonzero values
36+
fn example_mut_sparse_array(initial_nonzero_indices: [Field; 9], initial_nonzero_values: [Field; 9]) {
37+
let sparse_array_size = 10000;
38+
let mut array: MutSparseArray<10, Field> = MutSparseArray::create(nonzero_indices, nonzero_values, sparse_array_size);
39+
40+
// update element 1234 to contain value 9999
41+
array.set(1234, 9999);
42+
43+
// error, array can only contain 10 nonzero values
44+
array.ser(10, 888);
45+
}
46+
```
47+
48+
# Costs
49+
50+
Constructing arrays is proportional to the number of nonzero entries in the array and very small ~10 gates per element (plus the cost of initializing range tables if not already done so)
51+
52+
Reading from `SparseArray` is 14.5 gates
53+
Reading and writing to `MutSparseArray` is ~30 gates

src/lib.nr

Lines changed: 272 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,273 @@
1-
/// This doesn't really do anything by ensures that there is a test for CI to run.
2-
#[test]
3-
fn smoke_test() {
4-
assert(true);
1+
mod mut_sparse_array;
2+
use dep::sort::sort_advanced;
3+
4+
unconstrained fn __sort_field_as_u32(lhs: Field, rhs: Field) -> bool {
5+
// lhs.lt(rhs)
6+
lhs as u32 < rhs as u32
7+
}
8+
9+
fn assert_sorted(lhs: Field, rhs: Field) {
10+
let result = (rhs - lhs - 1);
11+
result.assert_max_bit_size(32);
12+
}
13+
14+
/**
15+
* @brief MutSparseArray, a sparse array of configurable size with `N` nonzero entries.
16+
* Can be read from and written into
17+
*
18+
* @param keys is size N+2 because we want to always ensure that,
19+
* for any valid index, there is some X where `keys[X] <= index <= keys[X+1]`
20+
* when constructing, we will set keys[0] = 0, and keys[N-1] = maximum - 1
21+
* @param values is size N+3 because of the following:
22+
* 1. keys[i] maps to values[i+1]
23+
* 2. values[0] is an empty object. when calling `get(idx)`, if `idx` is not in `keys` we will return `values[0]`
24+
**/
25+
struct MutSparseArrayBase<let N: u32, T, ComparisonFuncs>
26+
{
27+
values: [T; N + 3],
28+
keys: [Field; N + 2],
29+
linked_keys: [Field; N + 2],
30+
tail_ptr: Field,
31+
maximum: Field
32+
}
33+
34+
struct U32RangeTraits {
35+
}
36+
37+
struct MutSparseArray<let N: u32, T>
38+
{
39+
inner: MutSparseArrayBase<N, T, U32RangeTraits>
40+
}
41+
/**
42+
* @brief SparseArray, stores a sparse array of up to size 2^32 with `N` nonzero entries
43+
* SparseArray is constant i.e. values canot be inserted after creation.
44+
* See MutSparseArray for a mutable version (a bit more expensive)
45+
* @param keys is size N+2 because we want to always ensure that,
46+
* for any valid index, there is some X where `keys[X] <= index <= keys[X+1]`
47+
* when constructing, we will set keys[0] = 0, and keys[N-1] = maximum - 1
48+
* @param values is size N+3 because of the following:
49+
* 1. keys[i] maps to values[i+1]
50+
* 2. values[0] is an empty object. when calling `get(idx)`, if `idx` is not in `keys` we will return `values[0]`
51+
**/
52+
struct SparseArray<let N: u32, T> {
53+
keys: [Field; N + 2],
54+
values: [T; N + 3],
55+
maximum: Field // can be up to 2^32
56+
}
57+
impl<let N: u32, T> SparseArray<N, T> where T : std::default::Default {
58+
59+
/**
60+
* @brief construct a SparseArray
61+
**/
62+
fn create(_keys: [Field; N], _values: [T; N], size: Field) -> Self {
63+
let _maximum = size - 1;
64+
let mut r: Self = SparseArray { keys: [0; N + 2], values: [T::default(); N + 3], maximum: _maximum };
65+
66+
// for any valid index, we want to ensure the following is satified:
67+
// self.keys[X] <= index <= self.keys[X+1]
68+
// this requires us to sort hte keys, and insert a startpoint and endpoint
69+
let sorted_keys = sort_advanced(_keys, __sort_field_as_u32, assert_sorted);
70+
71+
// insert start and endpoints
72+
r.keys[0] = 0;
73+
for i in 0..N {
74+
r.keys[i+1] = sorted_keys.sorted[i];
75+
}
76+
r.keys[N+1] = _maximum;
77+
78+
// populate values based on the sorted keys
79+
// note: self.keys[i] maps to self.values[i+1]
80+
// self.values[0] does not map to any key. we use it to store the default empty value,
81+
// which is returned when `get(idx)` is called and `idx` does not exist in `self.keys`
82+
for i in 0..N {
83+
r.values[i+2] = _values[sorted_keys.sort_indices[i]];
84+
}
85+
// insert values that map to our key start and endpoints
86+
// if _keys[0] = 0 then values[0] must equal _values[0], so some conditional logic is required
87+
// (same for _keys[N-1])
88+
let mut initial_value = T::default();
89+
if (_keys[0] == 0) {
90+
initial_value = _values[0];
91+
}
92+
let mut final_value = T::default();
93+
if (_keys[N - 1] == _maximum) {
94+
final_value = _values[N-1];
95+
}
96+
r.values[1] = initial_value;
97+
r.values[N+2] = final_value;
98+
99+
// perform boundary checks!
100+
// the maximum size of the sparse array is 2^32
101+
// we need to check that every element in `self.keys` is less than 2^32
102+
// because `self.keys` is sorted, we can simply validate that
103+
// sorted_keys.sorted[0] < 2^32
104+
// sorted_keys.sorted[N-1] < maximum
105+
sorted_keys.sorted[0].assert_max_bit_size(32);
106+
_maximum.assert_max_bit_size(32);
107+
(_maximum - sorted_keys.sorted[N - 1]).assert_max_bit_size(32);
108+
r
109+
}
110+
111+
/**
112+
* @brief determine whether `target` is present in `self.keys`
113+
* @details if `found == false`, `self.keys[found_index] < target < self.keys[found_index + 1]`
114+
**/
115+
unconstrained fn search_for_key(self, target: Field) -> (Field, Field) {
116+
let mut found = false;
117+
let mut found_index = 0;
118+
let mut previous_less_than_or_equal_to_target = false;
119+
for i in 0..N + 2 {
120+
// if target = 0xffffffff we need to be able to add 1 here, so use u64
121+
let current_less_than_or_equal_to_target = self.keys[i] as u64 <= target as u64;
122+
if (self.keys[i] == target) {
123+
found = true;
124+
found_index = i as Field;
125+
break;
126+
}
127+
if (previous_less_than_or_equal_to_target & !current_less_than_or_equal_to_target) {
128+
found_index = i as Field - 1;
129+
break;
130+
}
131+
previous_less_than_or_equal_to_target = current_less_than_or_equal_to_target;
132+
}
133+
(found as Field, found_index)
134+
}
135+
136+
/**
137+
* @brief return element `idx` from the sparse array
138+
* @details cost is 14.5 gates per lookup
139+
**/
140+
fn get(self, idx: Field) -> T {
141+
let (found, found_index) = unsafe {
142+
self.search_for_key(idx)
143+
};
144+
// bool check. 0.25 gates cheaper than a raw `bool` type. need to fix at some point
145+
assert(found * found == found);
146+
147+
// OK! So we have the following cases to check
148+
// 1. if `found` then `self.keys[found_index] == idx`
149+
// 2. if `!found` then `self.keys[found_index] < idx < self.keys[found_index + 1]
150+
// how do we simplify these checks?
151+
// case 1 can be converted to `self.keys[found_index] <= idx <= self.keys[found_index]
152+
// case 2 can be modified to `self.keys[found_index] + 1 <= idx <= self.keys[found_index + 1] - 1
153+
// combine the two into the following single statement:
154+
// `self.keys[found_index] + 1 - found <= idx <= self.keys[found_index + 1 - found] - 1 + found
155+
let lhs = self.keys[found_index];
156+
let rhs = self.keys[found_index + 1 - found];
157+
let lhs_condition = idx - lhs - 1 + found;
158+
let rhs_condition = rhs - 1 + found - idx;
159+
lhs_condition.assert_max_bit_size(32);
160+
rhs_condition.assert_max_bit_size(32);
161+
162+
// self.keys[i] maps to self.values[i+1]
163+
// however...if we did not find a non-sparse entry, we want to return self.values[0] (the default value)
164+
let value_index = (found_index + 1) * found;
165+
self.values[value_index]
166+
}
167+
}
168+
169+
mod test {
170+
171+
use crate::SparseArray;
172+
#[test]
173+
fn test_sparse_lookup() {
174+
let example = SparseArray::create([1, 99, 7, 5], [123, 101112, 789, 456], 100);
175+
176+
assert(example.get(1) == 123);
177+
assert(example.get(5) == 456);
178+
assert(example.get(7) == 789);
179+
assert(example.get(99) == 101112);
180+
181+
for i in 0..100 {
182+
if ((i != 1) & (i != 5) & (i != 7) & (i != 99)) {
183+
assert(example.get(i as Field) == 0);
184+
}
185+
}
186+
}
187+
188+
#[test]
189+
fn test_sparse_lookup_boundary_cases() {
190+
// what about when keys[0] = 0 and keys[N-1] = 2^32 - 1?
191+
let example = SparseArray::create(
192+
[0, 99999, 7, 0xffffffff],
193+
[123, 101112, 789, 456],
194+
0x100000000
195+
);
196+
197+
assert(example.get(0) == 123);
198+
assert(example.get(99999) == 101112);
199+
assert(example.get(7) == 789);
200+
assert(example.get(0xffffffff) == 456);
201+
assert(example.get(0xfffffffe) == 0);
202+
}
203+
204+
#[test(should_fail_with = "call to assert_max_bit_size")]
205+
fn test_sparse_lookup_overflow() {
206+
let example = SparseArray::create([1, 5, 7, 99999], [123, 456, 789, 101112], 100000);
207+
208+
assert(example.get(100000) == 0);
209+
}
210+
211+
#[test(should_fail_with = "call to assert_max_bit_size")]
212+
fn test_sparse_lookup_boundary_case_overflow() {
213+
let example = SparseArray::create([0, 5, 7, 0xffffffff], [123, 456, 789, 101112], 0x100000000);
214+
215+
assert(example.get(0x100000000) == 0);
216+
}
217+
218+
#[test(should_fail_with = "call to assert_max_bit_size")]
219+
fn test_sparse_lookup_key_exceeds_maximum() {
220+
let example = SparseArray::create([0, 5, 7, 0xffffffff], [123, 456, 789, 101112], 0xffffffff);
221+
assert(example.maximum == 0xffffffff);
222+
}
223+
#[test]
224+
fn test_sparse_lookup_u32() {
225+
let example = SparseArray::create(
226+
[1, 99, 7, 5],
227+
[123 as u32, 101112 as u32, 789 as u32, 456 as u32],
228+
100
229+
);
230+
231+
assert(example.get(1) == 123);
232+
assert(example.get(5) == 456);
233+
assert(example.get(7) == 789);
234+
assert(example.get(99) == 101112);
235+
236+
for i in 0..100 {
237+
if ((i != 1) & (i != 5) & (i != 7) & (i != 99)) {
238+
assert(example.get(i as Field) == 0);
239+
}
240+
}
241+
}
242+
243+
struct F {
244+
foo: [Field; 3]
245+
}
246+
impl std::cmp::Eq for F {
247+
fn eq(self, other: Self) -> bool {
248+
self.foo == other.foo
249+
}
250+
}
251+
252+
impl std::default::Default for F {
253+
fn default() -> Self {
254+
F { foo: [0; 3] }
255+
}
256+
}
257+
258+
#[test]
259+
fn test_sparse_lookup_struct() {
260+
let values = [F { foo: [1, 2, 3] }, F { foo: [4, 5, 6] }, F { foo: [7, 8, 9] }, F { foo: [10, 11, 12] }];
261+
let example = SparseArray::create([1, 99, 7, 5], values, 100000);
262+
263+
assert(example.get(1) == values[0]);
264+
assert(example.get(5) == values[3]);
265+
assert(example.get(7) == values[2]);
266+
assert(example.get(99) == values[1]);
267+
for i in 0..100 {
268+
if ((i != 1) & (i != 5) & (i != 7) & (i != 99)) {
269+
assert(example.get(i as Field) == F::default());
270+
}
271+
}
272+
}
5273
}

0 commit comments

Comments
 (0)