Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
vector_field_push_span.test.cpp
Go to the documentation of this file.
2
6
7#include <gtest/gtest.h>
8#include <type_traits>
9#include <vector>
10
11namespace {
12
13using bb::fq;
14using bb::fr;
18
20
21// Boundary sizes around the lane width (SIZE=5): empty, partial, exact, and ragged multiples.
22constexpr std::array<size_t, 9> kSizes{ 0, 1, 4, 5, 6, 9, 10, 25, 31 };
23
24// push N field elements, then read every element back via the full-group operator[] (to_array) and
25// the trailing tail buffer; assert the round-trip is the identity.
26TEST(VectorFieldPushSpan, PushReadRoundTrip)
27{
28 constexpr size_t W = PushSpanFq::W;
29 for (size_t n : kSizes) {
30 std::vector<fq> src(n);
31 for (auto& v : src) {
32 v = fq::random_element(&engine);
33 }
34 std::vector<VecFq> backing((n / W) + 1);
35 PushSpanFq span{ std::span<VecFq>(backing) };
36 for (const auto& v : src) {
37 span.push(v);
38 }
39
40 ASSERT_EQ(span.size(), n);
41 ASSERT_EQ(span.num_full_vectors(), n / W);
42 ASSERT_EQ(span.tail(), n % W);
43
44 for (size_t i = 0; i < n; ++i) {
45 fq got;
46 if (i < span.num_full_vectors() * W) {
47 got = span[i / W].to_array()[i % W];
48 } else {
49 got = span.tail_data()[i % W];
50 }
51 EXPECT_EQ(got, src[i]) << "n=" << n << " i=" << i;
52 }
53 }
54}
55
56// reset() rewinds the cursor so the same backing storage is refilled cleanly across drains.
57TEST(VectorFieldPushSpan, ResetRefill)
58{
59 constexpr size_t W = PushSpanFq::W;
60 std::vector<VecFq> backing(4);
61 PushSpanFq span{ std::span<VecFq>(backing) };
62 for (size_t i = 0; i < 7; ++i) {
63 span.push(fq(i + 1));
64 }
65 span.reset();
66 EXPECT_EQ(span.size(), 0u);
67 EXPECT_EQ(span.tail(), 0u);
68
69 std::vector<fq> src(2 * W);
70 for (auto& v : src) {
71 v = fq::random_element(&engine);
72 }
73 for (const auto& v : src) {
74 span.push(v);
75 }
76 for (size_t i = 0; i < src.size(); ++i) {
77 EXPECT_EQ(span[i / W].to_array()[i % W], src[i]);
78 }
79}
80
81// Union-style check: the SAME elementwise expression run scalar vs over VectorFieldPushSpan groups
82// (VectorField arithmetic) must agree bit-for-bit — the property that lets one kernel serve both
83// canonical and packed backings.
84TEST(VectorFieldPushSpan, GroupwiseMatchesScalar)
85{
86 constexpr size_t W = PushSpanFq::W;
87 const size_t n = 4 * W; // exact multiple: all work in the full-group path
88 std::vector<fq> a(n), b(n), c(n);
89 for (size_t i = 0; i < n; ++i) {
90 a[i] = fq::random_element(&engine);
91 b[i] = fq::random_element(&engine);
92 c[i] = a[i] * b[i] + a[i]; // reference
93 }
94
95 std::vector<VecFq> ga(n / W), gb(n / W), gc(n / W);
96 PushSpanFq sa{ std::span<VecFq>(ga) };
97 PushSpanFq sb{ std::span<VecFq>(gb) };
98 PushSpanFq sc{ std::span<VecFq>(gc) };
99 for (size_t i = 0; i < n; ++i) {
100 sa.push(a[i]);
101 sb.push(b[i]);
102 sc.push(fq::zero());
103 }
104 for (size_t g = 0; g < sa.num_full_vectors(); ++g) {
105 sc[g] = sa[g] * sb[g] + sa[g];
106 }
107 for (size_t i = 0; i < n; ++i) {
108 EXPECT_EQ(sc[i / W].to_array()[i % W], c[i]) << "i=" << i;
109 }
110}
111
112// zip_for_each applies a generic field-arithmetic kernel across spans in lockstep (bulk + tail),
113// matching a scalar reference. The writer's kernel never mentions lanes, indices, or VectorField.
114TEST(VectorFieldPushSpan, ZipForEach)
115{
116 constexpr size_t W = PushSpanFq::W;
117 const size_t n = 2 * W + 3; // exercises full groups + a ragged tail
120 std::vector<fq> ref(n);
121 for (size_t i = 0; i < n; ++i) {
122 a[i] = fq::random_element(&engine);
123 b[i] = fq::random_element(&engine);
124 ref[i] = a[i] * b[i] + a[i];
125 }
126 std::vector<VecFq> back_a((n / W) + 1);
127 std::vector<VecFq> back_b((n / W) + 1);
128 std::vector<VecFq> back_o((n / W) + 1);
129 PushSpanFq sa{ std::span<VecFq>(back_a) };
130 PushSpanFq sb{ std::span<VecFq>(back_b) };
131 PushSpanFq so{ std::span<VecFq>(back_o) };
132 for (size_t i = 0; i < n; ++i) {
133 sa.push(a[i]);
134 sb.push(b[i]);
135 }
136
137 bb::zip_for_each(sa, sb, so, [](const auto& x, const auto& y, auto& o) { o = x * y + x; });
138 so.adopt_cursor(sa);
139
140 ASSERT_EQ(so.size(), n);
141 for (size_t i = 0; i < n; ++i) {
142 const fq got = (i < so.num_full_vectors() * W) ? so[i / W].to_array()[i % W] : so.tail_data()[i % W];
143 EXPECT_EQ(got, ref[i]) << "i=" << i;
144 }
145}
146
147// VectorAffineElementPushSpan feeds two coordinate cursors in lockstep.
148TEST(VectorAffineElementPushSpan, PushPointRoundTrip)
149{
151 constexpr size_t W = PushSpanFq::W;
152 const size_t n = 2 * W + 3;
153 std::vector<fq> xs(n), ys(n);
154 std::vector<VecFq> gx((n / W) + 1), gy((n / W) + 1);
155 Packed packed{ std::span<VecFq>(gx), std::span<VecFq>(gy) };
156 for (size_t i = 0; i < n; ++i) {
157 xs[i] = fq::random_element(&engine);
158 ys[i] = fq::random_element(&engine);
159 packed.push_point(xs[i], ys[i]);
160 }
161 ASSERT_EQ(packed.size(), n);
162 for (size_t i = 0; i < n; ++i) {
163 fq gx_i = (i < packed.num_full_vectors() * W) ? packed.x[i / W].to_array()[i % W] : packed.x.tail_data()[i % W];
164 fq gy_i = (i < packed.num_full_vectors() * W) ? packed.y[i / W].to_array()[i % W] : packed.y.tail_data()[i % W];
165 EXPECT_EQ(gx_i, xs[i]);
166 EXPECT_EQ(gy_i, ys[i]);
167 }
168}
169
170// Same suite over the scalar field, exercising the Bn254FrParams VectorField specialization.
171TEST(VectorFieldPushSpan, RoundTripFr)
172{
173 constexpr size_t W = PushSpanFr::W;
174 const size_t n = 3 * W + 2;
175 std::vector<fr> src(n);
177 PushSpanFr span{ std::span<bb::VectorField<bb::Bn254FrParams>>(backing) };
178 for (auto& v : src) {
180 span.push(v);
181 }
182 for (size_t i = 0; i < n; ++i) {
183 fr got = (i < span.num_full_vectors() * W) ? span[i / W].to_array()[i % W] : span.tail_data()[i % W];
184 EXPECT_EQ(got, src[i]);
185 }
186}
187
188// A span always aliases itself. The tail-only case is the one shares_backing() alone
189// misses (the tail lives in `partial`, not the backing), caught by the `this == &other` clause.
190TEST(VectorFieldPushSpan, AliasesSelfAtEveryFillLevel)
191{
192 constexpr size_t W = PushSpanFq::W;
193 for (size_t n : { size_t{ 0 }, W - 1, W, W + 1 }) { // empty, tail-only, exactly full, full+tail
194 std::vector<VecFq> backing((n / W) + 1);
195 PushSpanFq span{ std::span<VecFq>(backing) };
196 for (size_t i = 0; i < n; ++i) {
197 span.push(fq::random_element(&engine));
198 }
199 EXPECT_TRUE(span.aliases(span)) << "n=" << n;
200 }
201}
202
203// Two distinct spans over one backing alias only once a full VectorField occupies it;
204// while empty or tail-only, each span's elements live in its own `partial`.
205TEST(VectorFieldPushSpan, AliasesDistinctSpansSharingBacking)
206{
207 constexpr size_t W = PushSpanFq::W;
208 std::vector<VecFq> backing(4);
209 PushSpanFq a{ std::span<VecFq>(backing) };
210 PushSpanFq b{ std::span<VecFq>(backing) };
211
212 EXPECT_TRUE(a.shares_backing(b));
213 EXPECT_FALSE(a.aliases(b));
214
215 for (size_t i = 0; i < W - 1; ++i) {
216 a.push(fq::random_element(&engine));
217 }
218 EXPECT_FALSE(a.aliases(b)); // tail-only
219
220 a.push(fq::random_element(&engine)); // completes the first VectorField
221 EXPECT_TRUE(a.aliases(b));
222}
223
224TEST(VectorFieldPushSpan, SharesBackingDisjointVsSameStart)
225{
226 constexpr size_t W = PushSpanFq::W;
227 std::vector<VecFq> backing_a(4), backing_b(4);
228 PushSpanFq a{ std::span<VecFq>(backing_a) };
229 PushSpanFq b{ std::span<VecFq>(backing_b) };
230 PushSpanFq a_view{ std::span<VecFq>(backing_a) };
231
232 EXPECT_FALSE(a.shares_backing(b));
233 EXPECT_TRUE(a.shares_backing(a_view));
234
235 for (size_t i = 0; i < W; ++i) {
236 a.push(fq::random_element(&engine));
237 }
238 EXPECT_FALSE(a.aliases(b));
239}
240
241// F1: aliases()/shares_backing() compare only the start pointer, so spans over the same
242// buffer at different offsets are reported non-aliasing even when their ranges overlap.
243// TODO(pippenger-F1): switch to a range-overlap check if an offset-overlapping caller appears.
244TEST(VectorFieldPushSpan, AliasesMissesOffsetOverlap)
245{
246 constexpr size_t W = PushSpanFq::W;
247 std::vector<VecFq> backing(4);
248 PushSpanFq a{ std::span<VecFq>(backing) };
249 PushSpanFq b{ std::span<VecFq>(backing).subspan(1) };
250 for (size_t i = 0; i < 4 * W; ++i) {
251 a.push(fq::random_element(&engine));
252 }
253 EXPECT_FALSE(a.shares_backing(b));
254 EXPECT_FALSE(a.aliases(b));
255}
256
257// zip_for_each must auto-adopt the output cursor; this test omits the manual adopt_cursor
258// that the older ZipForEach test uses (which would mask a regression in the auto-adopt).
259TEST(VectorFieldPushSpan, ZipForEachAutoAdoptsOutputCursor)
260{
261 constexpr size_t W = PushSpanFq::W;
262 const size_t n = 2 * W + 3;
263 std::vector<fq> a(n), b(n), ref(n);
264 for (size_t i = 0; i < n; ++i) {
265 a[i] = fq::random_element(&engine);
266 b[i] = fq::random_element(&engine);
267 ref[i] = a[i] * b[i] + a[i];
268 }
269 std::vector<VecFq> back_a((n / W) + 1), back_b((n / W) + 1), back_o((n / W) + 1);
270 PushSpanFq sa{ std::span<VecFq>(back_a) };
271 PushSpanFq sb{ std::span<VecFq>(back_b) };
272 PushSpanFq so{ std::span<VecFq>(back_o) };
273 for (size_t i = 0; i < n; ++i) {
274 sa.push(a[i]);
275 sb.push(b[i]);
276 }
277
278 bb::zip_for_each(sa, sb, so, [](const auto& x, const auto& y, auto& o) { o = x * y + x; });
279
280 EXPECT_EQ(so.size(), n);
281 EXPECT_EQ(so.num_full_vectors(), n / W);
282 EXPECT_EQ(so.tail(), n % W);
283 for (size_t i = 0; i < n; ++i) {
284 const fq got = (i < so.num_full_vectors() * W) ? so[i / W].to_array()[i % W] : so.tail_data()[i % W];
285 EXPECT_EQ(got, ref[i]) << "i=" << i;
286 }
287}
288
289// Forward exclusive-prefix-product. Lane L of group g holds src[g*W + L], so for n = 2*W
290// each lane is the stream {src[L], src[W+L]}: out[0] = 1, out[1] = src[L], acc = src[L]*src[W+L].
291TEST(VectorFieldPushSpan, MapAccumulateForwardExclusivePrefixProduct)
292{
293 constexpr size_t W = PushSpanFq::W;
294 const size_t n = 2 * W;
295 std::vector<fq> src(n);
296 std::vector<VecFq> in_b(n / W), out_b(n / W);
297 PushSpanFq in{ std::span<VecFq>(in_b) };
298 PushSpanFq out{ std::span<VecFq>(out_b) };
299 for (size_t i = 0; i < n; ++i) {
300 src[i] = fq::random_element(&engine);
301 in.push(src[i]);
302 }
303
304 auto [bulk_acc, tail_acc] = bb::map_accumulate<bb::Direction::Forward>(
305 in, out, VecFq::broadcast(fq::one()), fq::one(), [](auto& acc, const auto& in_e, auto& out_e) {
306 out_e = acc;
307 acc = acc * in_e;
308 });
309
310 ASSERT_EQ(out.size(), n);
311 ASSERT_EQ(out.num_full_vectors(), 2u);
312 for (size_t L = 0; L < W; ++L) {
313 EXPECT_EQ(out[0].to_array()[L], fq::one()) << "L=" << L;
314 EXPECT_EQ(out[1].to_array()[L], src[L]) << "L=" << L;
315 EXPECT_EQ(bulk_acc.to_array()[L], src[L] * src[W + L]) << "L=" << L;
316 }
317 EXPECT_EQ(tail_acc, fq::one());
318}
319
320// Reverse mirrors the forward output: out[1] = 1, out[0] = src[W+L], acc = src[W+L]*src[L].
321TEST(VectorFieldPushSpan, MapAccumulateReverseExclusivePrefixProduct)
322{
323 constexpr size_t W = PushSpanFq::W;
324 const size_t n = 2 * W;
325 std::vector<fq> src(n);
326 std::vector<VecFq> in_b(n / W), out_b(n / W);
327 PushSpanFq in{ std::span<VecFq>(in_b) };
328 PushSpanFq out{ std::span<VecFq>(out_b) };
329 for (size_t i = 0; i < n; ++i) {
330 src[i] = fq::random_element(&engine);
331 in.push(src[i]);
332 }
333
334 auto [bulk_acc, tail_acc] = bb::map_accumulate<bb::Direction::Backward>(
335 in, out, VecFq::broadcast(fq::one()), fq::one(), [](auto& acc, const auto& in_e, auto& out_e) {
336 out_e = acc;
337 acc = acc * in_e;
338 });
339
340 ASSERT_EQ(out.size(), n);
341 for (size_t L = 0; L < W; ++L) {
342 EXPECT_EQ(out[1].to_array()[L], fq::one()) << "L=" << L;
343 EXPECT_EQ(out[0].to_array()[L], src[W + L]) << "L=" << L;
344 EXPECT_EQ(bulk_acc.to_array()[L], src[W + L] * src[L]) << "L=" << L;
345 }
346 EXPECT_EQ(tail_acc, fq::one());
347}
348
349// The tail threads its own accumulator. With n = W + 2: out_tail = {1, src[W]} and
350// tail_acc = src[W]*src[W+1], independent of the bulk group.
351TEST(VectorFieldPushSpan, MapAccumulateThreadsTailAccumulator)
352{
353 constexpr size_t W = PushSpanFq::W;
354 const size_t n = W + 2;
355 std::vector<fq> src(n);
356 std::vector<VecFq> in_b((n / W) + 1), out_b((n / W) + 1);
357 PushSpanFq in{ std::span<VecFq>(in_b) };
358 PushSpanFq out{ std::span<VecFq>(out_b) };
359 for (size_t i = 0; i < n; ++i) {
360 src[i] = fq::random_element(&engine);
361 in.push(src[i]);
362 }
363
364 auto [bulk_acc, tail_acc] = bb::map_accumulate<bb::Direction::Forward>(
365 in, out, VecFq::broadcast(fq::one()), fq::one(), [](auto& acc, const auto& in_e, auto& out_e) {
366 out_e = acc;
367 acc = acc * in_e;
368 });
369
370 ASSERT_EQ(out.size(), n);
371 ASSERT_EQ(out.tail(), 2u);
372 for (size_t L = 0; L < W; ++L) {
373 EXPECT_EQ(out[0].to_array()[L], fq::one()) << "L=" << L;
374 EXPECT_EQ(bulk_acc.to_array()[L], src[L]) << "L=" << L;
375 }
376 EXPECT_EQ(out.tail_data()[0], fq::one());
377 EXPECT_EQ(out.tail_data()[1], src[W]);
378 EXPECT_EQ(tail_acc, src[W] * src[W + 1]);
379}
380
381// Move-only: a copy would duplicate the fill cursor while sharing the backing.
382TEST(VectorFieldPushSpan, IsMoveOnly)
383{
385 static_assert(!std::is_copy_constructible_v<PushSpanFq>, "push-span must be move-only");
386 static_assert(!std::is_copy_assignable_v<PushSpanFq>, "push-span must be move-only");
387 static_assert(std::is_move_constructible_v<PushSpanFq>, "push-span must be movable");
388 static_assert(std::is_move_assignable_v<PushSpanFq>, "push-span must be movable");
389 static_assert(!std::is_copy_constructible_v<AffineSpan>, "affine push-span must be move-only");
390 static_assert(!std::is_copy_assignable_v<AffineSpan>, "affine push-span must be move-only");
391 static_assert(std::is_move_constructible_v<AffineSpan>, "affine push-span must be movable");
392 static_assert(std::is_move_assignable_v<AffineSpan>, "affine push-span must be movable");
393 SUCCEED();
394}
395
396} // namespace
TEST(acir_formal_proofs, uint_terms_add)
Tests 128-bit unsigned addition Verifies that the ACIR implementation of addition is correct Executio...
FF a
FF b
numeric::RNG & engine
RNG & get_debug_randomness(bool reset, std::uint_fast64_t seed)
Definition engine.cpp:245
field< Bn254FqParams > fq
Definition fq.hpp:153
void zip_for_each(Args &&... args)
constexpr void g(state_array &state, size_t a, size_t b, size_t c, size_t d, uint32_t x, uint32_t y)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
bb::VectorAffineElementPushSpan< BaseParams > out
static field random_element(numeric::RNG *engine=nullptr) noexcept