Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
vector_field.hpp
Go to the documentation of this file.
1#pragma once
2
3// VectorField: holds 5 field elements and processes them per call with a
4// batched kernel that interleaves one scalar stream (1 field) with one
5// quad-packed SIMD stream (4 fields in i32x4 / i64x2 lanes).
6//
7// This is a direct C++ port of the `q1s1` / `mix_s1q1` WAT kernels from
8// https://gist.github.com/AztecBot/b8e2e1d5c85d54e10fb34b48461361e0 (Mont-mul)
9// https://gist.github.com/AztecBot/2ad5f310fd0e8a3badda33487f4536ff (add/sub/eq/iz)
10//
11// Two critical constraints from the gist:
12//
13// 1. Karatsuba, not schoolbook.
14// Mont-mul splits 9 limbs into 5 (lo) + 4 (hi) and uses three schoolbook
15// products: 5x5 P_lo (25), 4x4 P_hi (16), 5x5 P_cross (25). Total 66 muls
16// in the product phase, NOT 81. Combined with 9 Yuval reductions × 9
17// madConst = 81, total is 66+81 = 147. Schoolbook would have been 81+81 =
18// 162. The gist: "it's ~25% of the total runtime."
19//
20// 2. Op-by-op interleaving.
21// Scalar statement, then equivalent quad statement, then next scalar, etc.
22// Clang preserves source order through its WASM backend, V8 TurboFan sees
23// adjacent different-opcode ops with independent operands, dispatches to
24// separate INT / SIMD pipes.
25//
26// Storage layout (WASM SIMD path): 9 × 29-bit limbs (R = 2^261)
27//
28// alignas(16) uint32_t scalar_data[9]; // one field, 9 × 29-bit limbs in u32.
29// // Top 3 bits of each u32 are always 0.
30// // Each read zero-extends to u64 at use
31// // site (mirroring the gist's
32// // `i64.extend_i32_u (i32.load …)`).
33// alignas(16) v128_t quad_data[9]; // 4 fields × 9 × 29-bit limbs,
34// // transposed: lane L of quad_data[k]
35// // = field L's u32 limb k
36//
37// Coarse invariant: each logical field is in [0, 2p) throughout.
38//
39// Storage layout (fallback): alignas(32) Field elts[5];
40
44
45#include <array>
46#include <cstddef>
47#include <cstdint>
48#include <cstring>
49#include <type_traits>
50
51#if defined(__wasm_simd128__)
52#include <wasm_simd128.h>
53#define BB_VECTOR_FIELD_SIMD 1
54#else
55#define BB_VECTOR_FIELD_SIMD 0
56#endif
57
58namespace bb {
59
60// `if constexpr`-friendly form of BB_VECTOR_FIELD_SIMD: lets dispatch sites stay templated instead of `#if`d.
62
63// ---------------------------------------------------------------------------
64// Compile-time constants derived from Params.
65// ---------------------------------------------------------------------------
66//
67// BN254 Fr's 9 × 29-bit wasm modulus (already in Params::modulus_wasm_*):
68// 0x10000001, 0x1f0fac9f, 0x0e5c2450, 0x07d090f3,
69// 0x1585d283, 0x02db40c0, 0x00a6e141, 0x0e5c2634, 0x0030644e
70//
71// twice_modulus_wasm[i] = 2 * modulus_wasm[i], propagated as 29-bit limbs.
72// Used for sub's r+2p blend and the TNM trick on add.
73
74template <class Params> inline constexpr std::array<uint64_t, 9> compute_twice_modulus_wasm() noexcept
75{
76 const std::array<uint64_t, 9> p = { Params::modulus_wasm_0, Params::modulus_wasm_1, Params::modulus_wasm_2,
77 Params::modulus_wasm_3, Params::modulus_wasm_4, Params::modulus_wasm_5,
78 Params::modulus_wasm_6, Params::modulus_wasm_7, Params::modulus_wasm_8 };
79 std::array<uint64_t, 9> twop{};
80 uint64_t carry = 0;
81 for (size_t i = 0; i < 9; ++i) {
82 uint64_t v = (p[i] << 1) + carry;
83 twop[i] = v & 0x1fffffff;
84 carry = v >> 29;
85 }
86 // No carry-out beyond limb 8 for BN254 Fr (2p fits in 9 × 29 bits easily).
87 return twop;
88}
89
90// 2^261 - 2p (mod 2^261), as 9 × 29-bit limbs. The "TNM" constant for add's
91// TNM-blend trick: a + b + TNM overflows 2^261 iff a + b >= 2p, which is the
92// exact condition under which we should reduce.
93template <class Params> inline constexpr std::array<uint64_t, 9> compute_tnm_wasm() noexcept
94{
95 const auto twop = compute_twice_modulus_wasm<Params>();
96 std::array<uint64_t, 9> tnm{};
97 uint64_t carry = 1;
98 for (size_t i = 0; i < 9; ++i) {
99 uint64_t v = ((~twop[i]) & 0x1fffffff) + carry;
100 tnm[i] = v & 0x1fffffff;
101 carry = v >> 29;
102 }
103 return tnm;
104}
105
106// Marker trait: specialized to true for each Params whose VectorField has a
107// SIMD operator* body (an explicit specialization defined in
108// vector_field_wasm.cpp). vectorized_for / vectorized_for_if read this to
109// decide whether to take the bulk path. Add a new specialization (below)
110// when adding the corresponding operator* body so the gate and the body
111// stay in lockstep.
112template <class Params> struct has_simd_mont_mul : std::false_type {};
113template <class Params> inline constexpr bool has_simd_mont_mul_v = has_simd_mont_mul<Params>::value;
114
115// Per-Params specializations. Forward-declared rather than #include'd so
116// vector_field.hpp stays Params-agnostic at the type level; the trait body
117// has no member access, only `: std::true_type`.
118class Bn254FrParams;
119class Bn254FqParams;
120template <> struct has_simd_mont_mul<Bn254FrParams> : std::true_type {};
121template <> struct has_simd_mont_mul<Bn254FqParams> : std::true_type {};
122
123// Single source of truth for consumers: is the packed SIMD mont-mul body actually compiled for
124// `Params` on this target? = SIMD target (BB_VECTOR_FIELD_SIMD) AND a Params that has a body
125// (has_simd_mont_mul_v). `has_simd_mont_mul_v` alone is target-blind (true for Bn254 even on native,
126// where no body exists), so consumers used to AND it with `#if defined(__wasm_simd128__)` at every call
127// site. Branch on this instead — it keeps the target preprocessor here at the data structure, and an
128// `if constexpr (simd_available_v<...>)` is false on native, so the packed branch is discarded there.
129template <class Params>
130inline constexpr bool simd_available_v = (BB_VECTOR_FIELD_SIMD != 0) && has_simd_mont_mul_v<Params>;
131
132template <class Params> struct alignas(32) VectorField {
135 // Relation algebra resolves View / CoefficientAccumulator / modulus off the element type, so
136 // VectorField must mirror those names from `field<Params>` to drop in as the element type.
139 static constexpr size_t SIZE = 5;
140 static constexpr auto modulus = Field::modulus;
141
142 // load_contiguous, store_contiguous, and the linear-memory ctor read/write
143 // raw Field bytes at fixed offsets (+32, +48, ...). Catch any future drift
144 // in field<Params>'s layout at compile time instead of as silent corruption
145 // at runtime.
146 static_assert(sizeof(field<Params>) == 32, "VectorField raw-byte transpose assumes sizeof(field<Params>) == 32");
147 static_assert(offsetof(field<Params>, data) == 0,
148 "VectorField raw-byte transpose assumes field::data is at offset 0");
149
150 static constexpr std::array<uint64_t, 9> P_WASM = { Params::modulus_wasm_0, Params::modulus_wasm_1,
151 Params::modulus_wasm_2, Params::modulus_wasm_3,
152 Params::modulus_wasm_4, Params::modulus_wasm_5,
153 Params::modulus_wasm_6, Params::modulus_wasm_7,
154 Params::modulus_wasm_8 };
155 static constexpr std::array<uint64_t, 9> TWOP_WASM = compute_twice_modulus_wasm<Params>();
156 static constexpr std::array<uint64_t, 9> TNM_WASM = compute_tnm_wasm<Params>();
157 // -(modulus)^-1 mod 2^29.
158 static constexpr uint64_t R_INV_MOD_2_29 = Params::r_inv & 0x1fffffffULL;
159 static constexpr std::array<uint64_t, 9> R_INV_WASM = {
160 Params::r_inv_wasm_0, Params::r_inv_wasm_1, Params::r_inv_wasm_2, Params::r_inv_wasm_3, Params::r_inv_wasm_4,
161 Params::r_inv_wasm_5, Params::r_inv_wasm_6, Params::r_inv_wasm_7, Params::r_inv_wasm_8
162 };
163
164 // ---- Storage ----
165#if BB_VECTOR_FIELD_SIMD
166 // 9 × 29-bit limbs, stored in u32 slots (top 3 bits zero). This matches
167 // bb::fr's internal WASM limb layout and compiles scalar-lane reads to the
168 // gist's `(i64.extend_i32_u (i32.load offset=… …))` pattern directly.
169 alignas(16) uint32_t scalar_data[9];
170 // 9 × v128; each v128 holds four u32 slots carrying one limb from each of
171 // 4 fields. Top 3 bits of each u32 are zero.
172 alignas(16) v128_t quad_data[9];
173#else
174 alignas(32) Field elts[5];
175#endif
176
177 constexpr VectorField() noexcept = default;
178
179 // Construct from 5 field<Params> values. Each is expected to be in the
180 // field's internal Montgomery form (R = 2^261 on WASM, R = 2^256 on
181 // native).
182 explicit VectorField(const std::array<Field, 5>& in) noexcept { store_from_array(in); }
183
184 // Implicit broadcast ctors -- mirror `field<Params>(int)`, `field<Params>(uint64_t)`, `field<Params>(Field)`
185 // so generic relation code like `q_arith_m - FF(1)` or multiplying an accumulator by a loop-invariant scalar
186 // constant (e.g. Poseidon round constants, typed as raw `fr`) compiles unchanged when FF=VectorField. The
187 // scalar value is broadcast to every lane.
188 VectorField(const Field& s) noexcept { *this = broadcast(s); }
189 VectorField(int s) noexcept { *this = broadcast(Field(s)); }
190 VectorField(uint64_t s) noexcept { *this = broadcast(Field(s)); }
191
192 static VectorField zero() noexcept { return broadcast(Field::zero()); }
193 static VectorField one() noexcept { return broadcast(Field::one()); }
194
195 VectorField sqr() const noexcept { return (*this) * (*this); }
196 void self_sqr() noexcept { *this = (*this) * (*this); }
197 VectorField operator-() const noexcept { return zero() - *this; }
198 // Per-lane scalar invert. Slow path -- for static-init constants and rare relation use. For batched
199 // inversions of independent values use the K=5 batch-inversion pattern in ecc/groups/element_impl.hpp.
201 {
202 auto a = to_array();
203 for (auto& v : a) {
204 v = v.invert();
205 }
206 return VectorField(a);
207 }
208
209 // Construct from 5 fields linear in memory (lane L = base[L] for L in
210 // 0..4). This is the canonical construction path used by the
211 // vectorised loop abstraction in place of `gather` — no random-access
212 // load, no scalar-pack staging, just a direct AoS→interleaved
213 // transpose driven by SIMD shuffles. See the out-of-class definition
214 // for the full SIMD pack chain.
215 explicit VectorField(const Field* base) noexcept;
216
217 // Write the 5 lanes back to 5 contiguous Fields in memory: base[L] =
218 // this->get(L) for L in 0..4. The matching write half of the linear-
219 // memory ctor — the loop abstraction calls this in place of `scatter`.
220 void store_to(Field* base) const noexcept;
221
223 {
226 return out;
227 }
228
230 {
232 Field sum = lanes[0];
233 for (size_t lane = 1; lane < SIZE; ++lane) {
234 sum += lanes[lane];
235 }
236 return sum;
237 }
238
239 // Test/debug helpers — both pack/unpack the full 5-lane payload. Do not
240 // use in hot code. For element-wise access use `to_array()` once and read
241 // the result; for write-back use `load_contiguous` / `store_contiguous`.
242 Field get(size_t i) const noexcept
243 {
244 auto a = to_array();
245 return a[i];
246 }
247 void set(size_t i, const Field& v) noexcept
248 {
249 auto a = to_array();
250 a[i] = v;
252 }
253
254 VectorField operator+(const VectorField& other) const noexcept;
255 VectorField operator-(const VectorField& other) const noexcept;
256 // Under WASM SIMD, operator* has explicit specializations for Bn254FrParams and Bn254FqParams
257 // (see vector_field_wasm.cpp); the has_simd_mont_mul trait above marks exactly those. Instantiating
258 // it with any other Params under SIMD is a link-time error. Gating it at compile time with a
259 // requires clause would change the symbol mangling and break the explicit-specialization match, so
260 // the declaration is left unconstrained and callers route unsupported Params through the scalar path.
261 VectorField operator*(const VectorField& other) const noexcept;
262
263 VectorField& operator+=(const VectorField& other) noexcept
264 {
265 *this = (*this) + other;
266 return *this;
267 }
268 VectorField& operator-=(const VectorField& other) noexcept
269 {
270 *this = (*this) - other;
271 return *this;
272 }
273 VectorField& operator*=(const VectorField& other) noexcept
274 {
275 *this = (*this) * other;
276 return *this;
277 }
278
279 // SLOW PATH — for random-access patterns only. For contiguous loads/stores
280 // use `load_contiguous` / `store_contiguous`, which avoid the per-lane
281 // scalar pack/unpack and dispatch a single SIMD shuffle.
282 //
283 // Gather: returns a VectorField whose lane L equals base[idx[L] - offset]. `offset` rebases absolute
284 // indices onto `base` (e.g. a polynomial's start_index), applied per-lane rather than as `base - offset`
285 // because forming that intermediate pointer is UB ([expr.add]/4) -- it lands before the array even though
286 // each base[idx[L] - offset] lands inside it.
287 static VectorField gather(const Field* base, std::array<size_t, 5> idx, size_t offset = 0) noexcept
288 {
289 std::array<Field, 5> tmp{ base[idx[0] - offset],
290 base[idx[1] - offset],
291 base[idx[2] - offset],
292 base[idx[3] - offset],
293 base[idx[4] - offset] };
294 return VectorField(tmp);
295 }
296
297 // Counterpart to `gather` for sources that aren't a flat `Field*` -- e.g. a polynomial's `operator[]`,
298 // which handles virtual-zero and start-index translation. lane L is set to value_at(L).
299 template <typename Fn> static VectorField from_lanes(const Fn& value_at) noexcept
300 {
302 for (size_t lane = 0; lane < SIZE; ++lane) {
303 lanes[lane] = value_at(lane);
304 }
305 return VectorField(lanes);
306 }
307
308 // SLOW PATH — see gather. Writes base[idx[L] - offset] = this->get(L) for L in 0..4.
309 void scatter(Field* base, std::array<size_t, 5> idx, size_t offset = 0) const noexcept
310 {
311 auto a = to_array();
312 base[idx[0] - offset] = a[0];
313 base[idx[1] - offset] = a[1];
314 base[idx[2] - offset] = a[2];
315 base[idx[3] - offset] = a[3];
316 base[idx[4] - offset] = a[4];
317 }
318
319 // Contiguous load: lane L = base[L] for L in 0..4.
320 // This is the fast path for vectorized_for<5>(...) bulk iterations where
321 // the 5 lanes are always consecutive addresses.
322 //
323 // Implementation (WASM SIMD, see out-of-class definition below): hand-
324 // fused 10 × wasm_v128_load on the raw Fr limb bytes, staged into an
325 // aligned 20 × u64 buffer, then pack_4u64_to_9x29 per lane. This beats
326 // gather's 20 × scalar random-access loads because V8 coalesces v128
327 // loads but not scalar loads at arbitrary addresses.
328 //
329 // Implementation (fallback): byte-for-byte copy into elts[5].
330 static VectorField load_contiguous(const Field* base) noexcept;
331
332 // Contiguous store: writes base[L] = this->get(L) for L in 0..4.
333 //
334 // Implementation (WASM SIMD, see out-of-class definition below): unpack
335 // into a 20 × u64 staging buffer, then emit 10 × wasm_v128_store. Same
336 // reasoning as load_contiguous — v128 stores coalesce, scalar stores at
337 // gather-scatter addresses don't.
338 void store_contiguous(Field* base) const noexcept;
339
340 // Broadcast a single Field to all 5 lanes. Much cheaper than the
341 // std::array-of-5 constructor, which re-packs the same value 5 times;
342 // this packs once and splats the 9 limbs across the 4 quad lanes.
343 static VectorField broadcast(const Field& s) noexcept;
344
345 // Mixed-type operators: broadcast scalar into a VectorField and delegate.
346 // [[gnu::always_inline]] is load-bearing under -Oz so the broadcast(s)
347 // call hoists out of the caller loop when s is loop-invariant.
348 [[gnu::always_inline]] friend VectorField operator+(VectorField v, const Field& s) noexcept
349 {
350 return v + broadcast(s);
351 }
352 [[gnu::always_inline]] friend VectorField operator+(const Field& s, VectorField v) noexcept
353 {
354 return broadcast(s) + v;
355 }
356 [[gnu::always_inline]] friend VectorField operator-(VectorField v, const Field& s) noexcept
357 {
358 return v - broadcast(s);
359 }
360 [[gnu::always_inline]] friend VectorField operator-(const Field& s, VectorField v) noexcept
361 {
362 return broadcast(s) - v;
363 }
364 [[gnu::always_inline]] friend VectorField operator*(VectorField v, const Field& s) noexcept
365 {
366 return v * broadcast(s);
367 }
368 [[gnu::always_inline]] friend VectorField operator*(const Field& s, VectorField v) noexcept
369 {
370 return broadcast(s) * v;
371 }
372
373 // Returns a 5-bit mask: bit 0 = scalar, bits 1..4 = quad lanes 0..3.
374 uint32_t eq_mask(const VectorField& other) const noexcept;
376
377 // Bool form: true iff EVERY lane satisfies the predicate. Prover relation gates coerce these to bool
378 // (e.g. `if (selector.is_zero()) skip`); a partially-non-zero lane pattern must read as not-zero, or
379 // the gate skips a subrelation that should fire on at least one of the rows packed into the lanes.
380 // The `_mask` form stays available for tests/microbenches that need per-lane visibility.
381 bool eq(const VectorField& other) const noexcept { return eq_mask(other) == 0b11111u; }
382 bool is_zero() const noexcept { return is_zero_mask() == 0b11111u; }
383
384 private:
385 void store_from_array(const std::array<Field, 5>& in) noexcept;
387};
388
389// =====================================================================
390// Implementation
391// =====================================================================
392
393#if BB_VECTOR_FIELD_SIMD
394
395namespace vector_field_detail {
396
397// Joint scalar+quad scheduling barriers. The gist's schedule requires
398// per-statement scalar/quad adjacency in the compiled WAT — V8 keeps v128
399// live-ranges short only when it sees scalar i64 ops interleaved with v128
400// ops on the issue queue. LLVM's WASM backend sees no dependency between
401// the two streams and will hoist one before the other absent a barrier.
402//
403// The "+r" inout form (not input-only) is load-bearing: it makes LLVM treat
404// each value as used AND re-defined, which breaks two specific opts:
405// 1. Stream-clumping by the instruction scheduler (all i64.mul first,
406// then all i64x2.extmul).
407// 2. CSE on `extend_low_u32x4(splat_const)` in the Yuval reductions,
408// which would otherwise collapse 9× fast extmul_low/high_u32x4 to
409// 9× slow i64x2.mul.
410// Input-only barriers (`asm volatile("" :: "r"(x) : "memory")`) don't force
411// re-definition and don't reliably break either. Measured: "+r" → ~30 ns/f,
412// input-only → ~33 ns/f.
413[[gnu::always_inline]] inline void bb_vf_barrier_sq(uint64_t& s, v128_t& q) noexcept
414{
415 asm volatile("" : "+r"(s), "+r"(q));
416}
417
418[[gnu::always_inline]] inline void bb_vf_barrier_sqq(uint64_t& s, v128_t& q_lo, v128_t& q_hi) noexcept
419{
420 asm volatile("" : "+r"(s), "+r"(q_lo), "+r"(q_hi));
421}
422
423// Pack 4 × u64 (little-endian 256-bit value) into 9 × 29-bit limbs, each stored
424// in a u32 slot.
425//
426// [[gnu::always_inline]] is load-bearing under -Oz: without it, the compiler
427// leaves pack/unpack as standalone calls, and the add_scaled hot loop pays
428// ~11 call overheads per block.
429[[gnu::always_inline]] inline void pack_4u64_to_9x29(const uint64_t in[4], uint32_t out[9]) noexcept
430{
431 out[0] = static_cast<uint32_t>(in[0] & 0x1fffffff);
432 out[1] = static_cast<uint32_t>((in[0] >> 29) & 0x1fffffff);
433 out[2] = static_cast<uint32_t>(((in[0] >> 58) & 0x3f) | ((in[1] & 0x7fffff) << 6));
434 out[3] = static_cast<uint32_t>((in[1] >> 23) & 0x1fffffff);
435 out[4] = static_cast<uint32_t>(((in[1] >> 52) & 0xfff) | ((in[2] & 0x1ffff) << 12));
436 out[5] = static_cast<uint32_t>((in[2] >> 17) & 0x1fffffff);
437 out[6] = static_cast<uint32_t>(((in[2] >> 46) & 0x3ffff) | ((in[3] & 0x7ff) << 18));
438 out[7] = static_cast<uint32_t>((in[3] >> 11) & 0x1fffffff);
439 out[8] = static_cast<uint32_t>((in[3] >> 40) & 0x1fffffff);
440}
441
442// Unpack 9 × 29-bit limbs (stored in u32 slots) back to 4 × u64. Each input
443// lane is zero-extended to u64 before shifting. [[gnu::always_inline]]
444// rationale: same as pack_4u64_to_9x29.
445[[gnu::always_inline]] inline void unpack_9x29_to_4u64(const uint32_t in[9], uint64_t out[4]) noexcept
446{
447 const uint64_t i0 = in[0], i1 = in[1], i2 = in[2], i3 = in[3], i4 = in[4];
448 const uint64_t i5 = in[5], i6 = in[6], i7 = in[7], i8 = in[8];
449 out[0] = i0 | (i1 << 29) | (i2 << 58);
450 out[1] = (i2 >> 6) | (i3 << 23) | (i4 << 52);
451 out[2] = (i4 >> 12) | (i5 << 17) | (i6 << 46);
452 out[3] = (i6 >> 18) | (i7 << 11) | (i8 << 40);
453}
454
455} // namespace vector_field_detail
456
457// -------------------- store/load --------------------
458
459template <class Params> inline void VectorField<Params>::store_from_array(const std::array<Field, 5>& in) noexcept
460{
461 // Scalar lane.
462 vector_field_detail::pack_4u64_to_9x29(in[0].data, scalar_data);
463 // Quad lanes: transpose 4 fields' 9-limb forms into 9 v128s.
464 uint32_t limbs[4][9];
465 vector_field_detail::pack_4u64_to_9x29(in[1].data, limbs[0]);
466 vector_field_detail::pack_4u64_to_9x29(in[2].data, limbs[1]);
467 vector_field_detail::pack_4u64_to_9x29(in[3].data, limbs[2]);
468 vector_field_detail::pack_4u64_to_9x29(in[4].data, limbs[3]);
469 for (size_t k = 0; k < 9; ++k) {
470 quad_data[k] = wasm_i32x4_make(static_cast<int32_t>(limbs[0][k]),
471 static_cast<int32_t>(limbs[1][k]),
472 static_cast<int32_t>(limbs[2][k]),
473 static_cast<int32_t>(limbs[3][k]));
474 }
475}
476
477template <class Params> inline void VectorField<Params>::load_to_array(std::array<Field, 5>& out) const noexcept
478{
479 vector_field_detail::unpack_9x29_to_4u64(scalar_data, out[0].data);
480 uint32_t limbs[4][9];
481 for (size_t k = 0; k < 9; ++k) {
482 limbs[0][k] = static_cast<uint32_t>(wasm_i32x4_extract_lane(quad_data[k], 0));
483 limbs[1][k] = static_cast<uint32_t>(wasm_i32x4_extract_lane(quad_data[k], 1));
484 limbs[2][k] = static_cast<uint32_t>(wasm_i32x4_extract_lane(quad_data[k], 2));
485 limbs[3][k] = static_cast<uint32_t>(wasm_i32x4_extract_lane(quad_data[k], 3));
486 }
487 vector_field_detail::unpack_9x29_to_4u64(limbs[0], out[1].data);
488 vector_field_detail::unpack_9x29_to_4u64(limbs[1], out[2].data);
489 vector_field_detail::unpack_9x29_to_4u64(limbs[2], out[3].data);
490 vector_field_detail::unpack_9x29_to_4u64(limbs[3], out[4].data);
491}
492
493// -------------------- broadcast --------------------
494//
495// Pack once, splat 9 times. Used by VectorField op Field mixed-type ops to
496// avoid the 5× pack the std::array constructor would do.
497
498template <class Params>
499[[gnu::always_inline]] inline VectorField<Params> VectorField<Params>::broadcast(const Field& s) noexcept
500{
501 VectorField result;
502 vector_field_detail::pack_4u64_to_9x29(s.data, result.scalar_data);
503 for (size_t k = 0; k < 9; ++k) {
504 result.quad_data[k] = wasm_i32x4_splat(static_cast<int32_t>(result.scalar_data[k]));
505 }
506 return result;
507}
508
509// -------------------- load_contiguous / store_contiguous --------------------
510//
511// Fast path for `vectorized_for<5>` bulk iterations: the 5 lanes always live
512// at consecutive Fr addresses, so we can transpose AoS → 9×29 interleaved
513// using only SIMD ops — no scalar pack and no staging round-trip through
514// memory.
515//
516// AoS layout (each Fr is 8 × u32 LE = 32 B): load 8 v128 (4 fields × 2 v128
517// per Fr), run two 4×4 i32 transposes (one over the four `lo` halves, one
518// over the four `hi` halves) so lane L of IN[m] = field (L+1)'s u32 chunk
519// m. Each 29-bit limb is then assembled in pure i32x4 ops (limbs 1..7 cost
520// 4 ops each, limbs 0 and 8 one each). Field 0 → scalar slot via the
521// standard scalar pack.
522//
523// i32x4 (current) beats an i64x2 pair-pack: one transpose covers all 4
524// quad fields, and no i64x2→i32x4 lane-merging at the end.
525
526namespace vector_field_detail {
527
528// 4×4 i32 transpose. Given 4 i32x4 vectors with logical layout:
529// row0 = { a0, a1, a2, a3 }
530// row1 = { b0, b1, b2, b3 }
531// row2 = { c0, c1, c2, c3 }
532// row3 = { d0, d1, d2, d3 }
533// produce:
534// col0 = { a0, b0, c0, d0 } col1 = { a1, b1, c1, d1 }
535// col2 = { a2, b2, c2, d2 } col3 = { a3, b3, c3, d3 }
536// in 8 i32x4 shuffles.
537[[gnu::always_inline]] inline void transpose_4x4_i32x4(
538 v128_t r0, v128_t r1, v128_t r2, v128_t r3, v128_t& c0, v128_t& c1, v128_t& c2, v128_t& c3) noexcept
539{
540 const v128_t t0 = wasm_i32x4_shuffle(r0, r1, 0, 4, 1, 5); // {a0,b0,a1,b1}
541 const v128_t t1 = wasm_i32x4_shuffle(r0, r1, 2, 6, 3, 7); // {a2,b2,a3,b3}
542 const v128_t t2 = wasm_i32x4_shuffle(r2, r3, 0, 4, 1, 5); // {c0,d0,c1,d1}
543 const v128_t t3 = wasm_i32x4_shuffle(r2, r3, 2, 6, 3, 7); // {c2,d2,c3,d3}
544 c0 = wasm_i32x4_shuffle(t0, t2, 0, 1, 4, 5); // {a0,b0,c0,d0}
545 c1 = wasm_i32x4_shuffle(t0, t2, 2, 3, 6, 7); // {a1,b1,c1,d1}
546 c2 = wasm_i32x4_shuffle(t1, t3, 0, 1, 4, 5); // {a2,b2,c2,d2}
547 c3 = wasm_i32x4_shuffle(t1, t3, 2, 3, 6, 7); // {a3,b3,c3,d3}
548}
549
550} // namespace vector_field_detail
551
552template <class Params> [[gnu::always_inline]] inline VectorField<Params>::VectorField(const Field* base) noexcept
553{
554 // Field 0 → scalar slot (plain scalar pack on the raw u64 limbs).
555 vector_field_detail::pack_4u64_to_9x29(base[0].data, scalar_data);
556
557 // Fields 1..4 → quad lanes via a single i32x4 transpose-then-pack chain.
558 const uint8_t* p = reinterpret_cast<const uint8_t*>(base);
559 const v128_t f1_lo = wasm_v128_load(p + 32);
560 const v128_t f1_hi = wasm_v128_load(p + 48);
561 const v128_t f2_lo = wasm_v128_load(p + 64);
562 const v128_t f2_hi = wasm_v128_load(p + 80);
563 const v128_t f3_lo = wasm_v128_load(p + 96);
564 const v128_t f3_hi = wasm_v128_load(p + 112);
565 const v128_t f4_lo = wasm_v128_load(p + 128);
566 const v128_t f4_hi = wasm_v128_load(p + 144);
567
568 // Two 4×4 transposes (8 + 8 shuffles). After this, lane L of IN[m]
569 // holds field (L+1)'s u32 chunk m for m in 0..7.
570 v128_t IN0, IN1, IN2, IN3, IN4, IN5, IN6, IN7;
571 vector_field_detail::transpose_4x4_i32x4(f1_lo, f2_lo, f3_lo, f4_lo, IN0, IN1, IN2, IN3);
572 vector_field_detail::transpose_4x4_i32x4(f1_hi, f2_hi, f3_hi, f4_hi, IN4, IN5, IN6, IN7);
573
574 // 29-bit limb assembly. Each 29-bit limb k spans bits [29k .. 29k+28] of
575 // the 256-bit number. Within an 8 × u32 representation the boundary
576 // 29k mod 32 / 29k / 32 lookup is:
577 //
578 // limb bit-range u32 chunk lo-shift hi-shift bits-from-hi
579 // 0 [ 0 .. 28] IN0 0 — 0
580 // 1 [ 29 .. 57] IN0/IN1 29 3 26
581 // 2 [ 58 .. 86] IN1/IN2 26 6 23
582 // 3 [ 87 .. 115] IN2/IN3 23 9 20
583 // 4 [116 .. 144] IN3/IN4 20 12 17
584 // 5 [145 .. 173] IN4/IN5 17 15 14
585 // 6 [174 .. 202] IN5/IN6 14 18 11
586 // 7 [203 .. 231] IN6/IN7 11 21 8
587 // 8 [232 .. 260] IN7 8 — 0 (24-bit max)
588 //
589 // For limbs 1..7 we compute `((lo >> lo_shift) | (hi << hi_shift)) & MASK29`.
590 // The trailing AND lets us skip masking the source `hi`.
591 const v128_t MASK29 = wasm_i32x4_splat(0x1fffffff);
592 quad_data[0] = wasm_v128_and(IN0, MASK29);
593 quad_data[1] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN0, 29), wasm_i32x4_shl(IN1, 3)), MASK29);
594 quad_data[2] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN1, 26), wasm_i32x4_shl(IN2, 6)), MASK29);
595 quad_data[3] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN2, 23), wasm_i32x4_shl(IN3, 9)), MASK29);
596 quad_data[4] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN3, 20), wasm_i32x4_shl(IN4, 12)), MASK29);
597 quad_data[5] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN4, 17), wasm_i32x4_shl(IN5, 15)), MASK29);
598 quad_data[6] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN5, 14), wasm_i32x4_shl(IN6, 18)), MASK29);
599 quad_data[7] = wasm_v128_and(wasm_v128_or(wasm_u32x4_shr(IN6, 11), wasm_i32x4_shl(IN7, 21)), MASK29);
600 // limb 8: top 24 bits of IN7, no mask needed (BN254 Fr coarse form is
601 // < 2 * p < 2^255, so the top u32 has at most 23 set bits and bits
602 // 24..31 are zero; shifting right by 8 leaves at most 24 bits set).
603 quad_data[8] = wasm_u32x4_shr(IN7, 8);
604}
605
606template <class Params>
607[[gnu::always_inline]] inline VectorField<Params> VectorField<Params>::load_contiguous(const Field* base) noexcept
608{
609 return VectorField(base);
610}
611
612template <class Params>
613[[gnu::always_inline]] inline void VectorField<Params>::store_contiguous(Field* base) const noexcept
614{
615 // Field 0 ← scalar slot.
616 vector_field_detail::unpack_9x29_to_4u64(scalar_data, base[0].data);
617
618 // Fields 1..4 ← quad lanes. Inverse of the load:
619 // 1. Reassemble each output u32 chunk as i32x4 (lane L = field (L+1)'s
620 // chunk m), splicing two adjacent 29-bit limbs.
621 // 2. Run two 4×4 i32 transposes to demux the 8 chunks back into one
622 // lo + hi v128 per Fr.
623 //
624 // u32 chunk → 29-bit limb mapping (inverse of the load table):
625 //
626 // chunk bit-range from limbs lo>>shift hi<<shift
627 // 0 [ 0 .. 31] l0/l1 0 29
628 // 1 [ 32 .. 63] l1/l2 3 26
629 // 2 [ 64 .. 95] l2/l3 6 23
630 // 3 [ 96 .. 127] l3/l4 9 20
631 // 4 [128 .. 159] l4/l5 12 17
632 // 5 [160 .. 191] l5/l6 15 14
633 // 6 [192 .. 223] l6/l7 18 11
634 // 7 [224 .. 255] l7/l8 21 8
635 //
636 // No mask needed on the result: each (limb >> hi_shift) << shift_lo cannot
637 // exceed 32 bits because we shift the incoming 29-bit limb left by ≤21
638 // and OR it on top of bits ≤21 of the running u32 — bit 32 is never set.
639
640 const v128_t l0 = quad_data[0], l1 = quad_data[1], l2 = quad_data[2];
641 const v128_t l3 = quad_data[3], l4 = quad_data[4], l5 = quad_data[5];
642 const v128_t l6 = quad_data[6], l7 = quad_data[7], l8 = quad_data[8];
643
644 const v128_t OUT0 = wasm_v128_or(l0, wasm_i32x4_shl(l1, 29));
645 const v128_t OUT1 = wasm_v128_or(wasm_u32x4_shr(l1, 3), wasm_i32x4_shl(l2, 26));
646 const v128_t OUT2 = wasm_v128_or(wasm_u32x4_shr(l2, 6), wasm_i32x4_shl(l3, 23));
647 const v128_t OUT3 = wasm_v128_or(wasm_u32x4_shr(l3, 9), wasm_i32x4_shl(l4, 20));
648 const v128_t OUT4 = wasm_v128_or(wasm_u32x4_shr(l4, 12), wasm_i32x4_shl(l5, 17));
649 const v128_t OUT5 = wasm_v128_or(wasm_u32x4_shr(l5, 15), wasm_i32x4_shl(l6, 14));
650 const v128_t OUT6 = wasm_v128_or(wasm_u32x4_shr(l6, 18), wasm_i32x4_shl(l7, 11));
651 const v128_t OUT7 = wasm_v128_or(wasm_u32x4_shr(l7, 21), wasm_i32x4_shl(l8, 8));
652
653 // 4×4 transpose of (OUT0..3) → (f1_lo, f2_lo, f3_lo, f4_lo).
654 // 4×4 transpose of (OUT4..7) → (f1_hi, f2_hi, f3_hi, f4_hi).
655 v128_t f1_lo, f2_lo, f3_lo, f4_lo, f1_hi, f2_hi, f3_hi, f4_hi;
656 vector_field_detail::transpose_4x4_i32x4(OUT0, OUT1, OUT2, OUT3, f1_lo, f2_lo, f3_lo, f4_lo);
657 vector_field_detail::transpose_4x4_i32x4(OUT4, OUT5, OUT6, OUT7, f1_hi, f2_hi, f3_hi, f4_hi);
658
659 uint8_t* dst = reinterpret_cast<uint8_t*>(base);
660 wasm_v128_store(dst + 32, f1_lo);
661 wasm_v128_store(dst + 48, f1_hi);
662 wasm_v128_store(dst + 64, f2_lo);
663 wasm_v128_store(dst + 80, f2_hi);
664 wasm_v128_store(dst + 96, f3_lo);
665 wasm_v128_store(dst + 112, f3_hi);
666 wasm_v128_store(dst + 128, f4_lo);
667 wasm_v128_store(dst + 144, f4_hi);
668}
669
670// Linear-memory store paired with the linear-memory ctor above.
671// `store_contiguous` is the canonical writer; `store_to` is the matching
672// member-named entry point used by the loop abstraction.
673template <class Params> [[gnu::always_inline]] inline void VectorField<Params>::store_to(Field* base) const noexcept
674{
675 store_contiguous(base);
676}
677
678// -------------------- operator+ (coarse form, 9x29 limbs) --------------------
679//
680// Two independent chains (the "TNM trick"):
681// r[k] = a[k] + b[k] (+ carry from r[k-1]) ; raw add
682// t[k] = r[k] + TNM[k] (+ carry from t[k-1]) ; independent chain
683// if t produces a final carry (i.e., a+b >= 2p) use t, else use r.
684//
685// Interleaved scalar / quad. Each 29-bit limb + 29-bit limb + 1 <= 30 bits so
686// carries fit in bit 29.
687
688template <class Params>
689[[gnu::always_inline]] inline VectorField<Params> VectorField<Params>::operator+(
690 const VectorField& other) const noexcept
691{
692 constexpr uint64_t MASK = 0x1fffffffULL;
693 const v128_t mask_splat = wasm_i32x4_splat(MASK);
694
695 VectorField result;
696
697 // --- r chain: r = a + b with carry, limbs 0..8 ---
698 uint64_t sr0 = scalar_data[0] + other.scalar_data[0];
699 v128_t qr0 = wasm_i32x4_add(quad_data[0], other.quad_data[0]);
700 uint64_t scarry = sr0 >> 29;
701 v128_t qcarry = wasm_u32x4_shr(qr0, 29);
702 sr0 &= MASK;
703 qr0 = wasm_v128_and(qr0, mask_splat);
704
705 uint64_t sr1 = scalar_data[1] + other.scalar_data[1] + scarry;
706 v128_t qr1 = wasm_i32x4_add(wasm_i32x4_add(quad_data[1], other.quad_data[1]), qcarry);
707 scarry = sr1 >> 29;
708 qcarry = wasm_u32x4_shr(qr1, 29);
709 sr1 &= MASK;
710 qr1 = wasm_v128_and(qr1, mask_splat);
711 vector_field_detail::bb_vf_barrier_sqq(sr1, qr1, qcarry);
712 asm volatile("" : "+r"(scarry));
713
714 uint64_t sr2 = scalar_data[2] + other.scalar_data[2] + scarry;
715 v128_t qr2 = wasm_i32x4_add(wasm_i32x4_add(quad_data[2], other.quad_data[2]), qcarry);
716 scarry = sr2 >> 29;
717 qcarry = wasm_u32x4_shr(qr2, 29);
718 sr2 &= MASK;
719 qr2 = wasm_v128_and(qr2, mask_splat);
720 vector_field_detail::bb_vf_barrier_sqq(sr2, qr2, qcarry);
721 asm volatile("" : "+r"(scarry));
722
723 uint64_t sr3 = scalar_data[3] + other.scalar_data[3] + scarry;
724 v128_t qr3 = wasm_i32x4_add(wasm_i32x4_add(quad_data[3], other.quad_data[3]), qcarry);
725 scarry = sr3 >> 29;
726 qcarry = wasm_u32x4_shr(qr3, 29);
727 sr3 &= MASK;
728 qr3 = wasm_v128_and(qr3, mask_splat);
729 vector_field_detail::bb_vf_barrier_sqq(sr3, qr3, qcarry);
730 asm volatile("" : "+r"(scarry));
731
732 uint64_t sr4 = scalar_data[4] + other.scalar_data[4] + scarry;
733 v128_t qr4 = wasm_i32x4_add(wasm_i32x4_add(quad_data[4], other.quad_data[4]), qcarry);
734 scarry = sr4 >> 29;
735 qcarry = wasm_u32x4_shr(qr4, 29);
736 sr4 &= MASK;
737 qr4 = wasm_v128_and(qr4, mask_splat);
738 vector_field_detail::bb_vf_barrier_sqq(sr4, qr4, qcarry);
739 asm volatile("" : "+r"(scarry));
740
741 uint64_t sr5 = scalar_data[5] + other.scalar_data[5] + scarry;
742 v128_t qr5 = wasm_i32x4_add(wasm_i32x4_add(quad_data[5], other.quad_data[5]), qcarry);
743 scarry = sr5 >> 29;
744 qcarry = wasm_u32x4_shr(qr5, 29);
745 sr5 &= MASK;
746 qr5 = wasm_v128_and(qr5, mask_splat);
747 vector_field_detail::bb_vf_barrier_sqq(sr5, qr5, qcarry);
748 asm volatile("" : "+r"(scarry));
749
750 uint64_t sr6 = scalar_data[6] + other.scalar_data[6] + scarry;
751 v128_t qr6 = wasm_i32x4_add(wasm_i32x4_add(quad_data[6], other.quad_data[6]), qcarry);
752 scarry = sr6 >> 29;
753 qcarry = wasm_u32x4_shr(qr6, 29);
754 sr6 &= MASK;
755 qr6 = wasm_v128_and(qr6, mask_splat);
756 vector_field_detail::bb_vf_barrier_sqq(sr6, qr6, qcarry);
757 asm volatile("" : "+r"(scarry));
758
759 uint64_t sr7 = scalar_data[7] + other.scalar_data[7] + scarry;
760 v128_t qr7 = wasm_i32x4_add(wasm_i32x4_add(quad_data[7], other.quad_data[7]), qcarry);
761 scarry = sr7 >> 29;
762 qcarry = wasm_u32x4_shr(qr7, 29);
763 sr7 &= MASK;
764 qr7 = wasm_v128_and(qr7, mask_splat);
765 vector_field_detail::bb_vf_barrier_sqq(sr7, qr7, qcarry);
766 asm volatile("" : "+r"(scarry));
767
768 uint64_t sr8 = scalar_data[8] + other.scalar_data[8] + scarry;
769 v128_t qr8 = wasm_i32x4_add(wasm_i32x4_add(quad_data[8], other.quad_data[8]), qcarry);
770 // No carry out of limb 8 for coarse inputs + add (result < 2 * 2p < 2^261).
771
772 // --- t chain: t = r + TNM with carry, limbs 0..8 ---
773 uint64_t st0 = sr0 + TNM_WASM[0];
774 v128_t qt0 = wasm_i32x4_add(qr0, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[0])));
775 scarry = st0 >> 29;
776 qcarry = wasm_u32x4_shr(qt0, 29);
777 st0 &= MASK;
778 qt0 = wasm_v128_and(qt0, mask_splat);
779 vector_field_detail::bb_vf_barrier_sqq(st0, qt0, qcarry);
780 asm volatile("" : "+r"(scarry));
781
782 uint64_t st1 = sr1 + TNM_WASM[1] + scarry;
783 v128_t qt1 = wasm_i32x4_add(wasm_i32x4_add(qr1, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[1]))), qcarry);
784 scarry = st1 >> 29;
785 qcarry = wasm_u32x4_shr(qt1, 29);
786 st1 &= MASK;
787 qt1 = wasm_v128_and(qt1, mask_splat);
788 vector_field_detail::bb_vf_barrier_sqq(st1, qt1, qcarry);
789 asm volatile("" : "+r"(scarry));
790
791 uint64_t st2 = sr2 + TNM_WASM[2] + scarry;
792 v128_t qt2 = wasm_i32x4_add(wasm_i32x4_add(qr2, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[2]))), qcarry);
793 scarry = st2 >> 29;
794 qcarry = wasm_u32x4_shr(qt2, 29);
795 st2 &= MASK;
796 qt2 = wasm_v128_and(qt2, mask_splat);
797 vector_field_detail::bb_vf_barrier_sqq(st2, qt2, qcarry);
798 asm volatile("" : "+r"(scarry));
799
800 uint64_t st3 = sr3 + TNM_WASM[3] + scarry;
801 v128_t qt3 = wasm_i32x4_add(wasm_i32x4_add(qr3, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[3]))), qcarry);
802 scarry = st3 >> 29;
803 qcarry = wasm_u32x4_shr(qt3, 29);
804 st3 &= MASK;
805 qt3 = wasm_v128_and(qt3, mask_splat);
806 vector_field_detail::bb_vf_barrier_sqq(st3, qt3, qcarry);
807 asm volatile("" : "+r"(scarry));
808
809 uint64_t st4 = sr4 + TNM_WASM[4] + scarry;
810 v128_t qt4 = wasm_i32x4_add(wasm_i32x4_add(qr4, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[4]))), qcarry);
811 scarry = st4 >> 29;
812 qcarry = wasm_u32x4_shr(qt4, 29);
813 st4 &= MASK;
814 qt4 = wasm_v128_and(qt4, mask_splat);
815 vector_field_detail::bb_vf_barrier_sqq(st4, qt4, qcarry);
816 asm volatile("" : "+r"(scarry));
817
818 uint64_t st5 = sr5 + TNM_WASM[5] + scarry;
819 v128_t qt5 = wasm_i32x4_add(wasm_i32x4_add(qr5, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[5]))), qcarry);
820 scarry = st5 >> 29;
821 qcarry = wasm_u32x4_shr(qt5, 29);
822 st5 &= MASK;
823 qt5 = wasm_v128_and(qt5, mask_splat);
824 vector_field_detail::bb_vf_barrier_sqq(st5, qt5, qcarry);
825 asm volatile("" : "+r"(scarry));
826
827 uint64_t st6 = sr6 + TNM_WASM[6] + scarry;
828 v128_t qt6 = wasm_i32x4_add(wasm_i32x4_add(qr6, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[6]))), qcarry);
829 scarry = st6 >> 29;
830 qcarry = wasm_u32x4_shr(qt6, 29);
831 st6 &= MASK;
832 qt6 = wasm_v128_and(qt6, mask_splat);
833 vector_field_detail::bb_vf_barrier_sqq(st6, qt6, qcarry);
834 asm volatile("" : "+r"(scarry));
835
836 uint64_t st7 = sr7 + TNM_WASM[7] + scarry;
837 v128_t qt7 = wasm_i32x4_add(wasm_i32x4_add(qr7, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[7]))), qcarry);
838 scarry = st7 >> 29;
839 qcarry = wasm_u32x4_shr(qt7, 29);
840 st7 &= MASK;
841 qt7 = wasm_v128_and(qt7, mask_splat);
842 vector_field_detail::bb_vf_barrier_sqq(st7, qt7, qcarry);
843 asm volatile("" : "+r"(scarry));
844
845 uint64_t st8 = sr8 + TNM_WASM[8] + scarry;
846 v128_t qt8 = wasm_i32x4_add(wasm_i32x4_add(qr8, wasm_i32x4_splat(static_cast<int32_t>(TNM_WASM[8]))), qcarry);
847 // Top-limb carry: if t8 >= 2^29, a+b >= 2p — pick t (reduced). Else pick r.
848 const uint64_t sc_final = st8 >> 29;
849 const v128_t qc_final = wasm_u32x4_shr(qt8, 29);
850 st8 &= MASK;
851 qt8 = wasm_v128_and(qt8, mask_splat);
852
853 // Blend: sc_final nonzero => pick t.
854 const uint64_t smask = 0ULL - sc_final;
855 // qc_final is 0/1 per lane; turn into 0 / all-ones via compare-not-equal-0.
856 // Using i32x4_eq (qc_final, 1) = all-ones if lane is 1, which is correct.
857 const v128_t qmask = wasm_i32x4_eq(qc_final, wasm_i32x4_splat(1));
858 const uint64_t simask = ~smask;
859
860 result.scalar_data[0] = static_cast<uint32_t>((sr0 & simask) | (st0 & smask));
861 result.quad_data[0] = wasm_v128_bitselect(qt0, qr0, qmask);
862 result.scalar_data[1] = static_cast<uint32_t>((sr1 & simask) | (st1 & smask));
863 result.quad_data[1] = wasm_v128_bitselect(qt1, qr1, qmask);
864 result.scalar_data[2] = static_cast<uint32_t>((sr2 & simask) | (st2 & smask));
865 result.quad_data[2] = wasm_v128_bitselect(qt2, qr2, qmask);
866 result.scalar_data[3] = static_cast<uint32_t>((sr3 & simask) | (st3 & smask));
867 result.quad_data[3] = wasm_v128_bitselect(qt3, qr3, qmask);
868 result.scalar_data[4] = static_cast<uint32_t>((sr4 & simask) | (st4 & smask));
869 result.quad_data[4] = wasm_v128_bitselect(qt4, qr4, qmask);
870 result.scalar_data[5] = static_cast<uint32_t>((sr5 & simask) | (st5 & smask));
871 result.quad_data[5] = wasm_v128_bitselect(qt5, qr5, qmask);
872 result.scalar_data[6] = static_cast<uint32_t>((sr6 & simask) | (st6 & smask));
873 result.quad_data[6] = wasm_v128_bitselect(qt6, qr6, qmask);
874 result.scalar_data[7] = static_cast<uint32_t>((sr7 & simask) | (st7 & smask));
875 result.quad_data[7] = wasm_v128_bitselect(qt7, qr7, qmask);
876 result.scalar_data[8] = static_cast<uint32_t>((sr8 & simask) | (st8 & smask));
877 result.quad_data[8] = wasm_v128_bitselect(qt8, qr8, qmask);
878
879 return result;
880}
881
882// -------------------- operator- (coarse form, 9x29 limbs) --------------------
883//
884// Two chains:
885// r = a - b (may go negative)
886// s = r + 2p (always in [0, 4p))
887// If final borrow from r is set, pick s; else pick r.
888
889template <class Params>
890[[gnu::always_inline]] inline VectorField<Params> VectorField<Params>::operator-(
891 const VectorField& other) const noexcept
892{
893 constexpr uint64_t MASK = 0x1fffffffULL;
894 const v128_t mask_splat = wasm_i32x4_splat(MASK);
895
896 VectorField result;
897
898 // Strategy: keep the r chain fully in i32x4 space (no i64x2 transitions).
899 // - For limb 0, compute sub and borrow from underflow.
900 // - Represent quad borrow as an i32x4 "0 or 1" value.
901 // - Subsequent limbs: r[k] = a[k] - b[k] - borrow, mask to 29 bits,
902 // borrow_out = 1 iff underflow. Detect underflow via the top bit (bit 31)
903 // of the raw subtract result, since 29-bit values keep bits 29..31 clear
904 // and underflow sets them.
905
906 // Limb 0.
907 int64_t sdiff0 = static_cast<int64_t>(scalar_data[0]) - static_cast<int64_t>(other.scalar_data[0]);
908 v128_t qdiff0 = wasm_i32x4_sub(quad_data[0], other.quad_data[0]);
909 uint64_t sr0 = static_cast<uint64_t>(sdiff0) & MASK;
910 v128_t qr0 = wasm_v128_and(qdiff0, mask_splat);
911 // Borrow: scalar 0 or 1; quad: 1 per lane if underflow (top bit of i32 set).
912 int64_t sborrow = (sdiff0 < 0) ? 1 : 0;
913 v128_t qborrow = wasm_u32x4_shr(qdiff0, 31);
914
915 // Limb 1.
916 int64_t sdiff1 = static_cast<int64_t>(scalar_data[1]) - static_cast<int64_t>(other.scalar_data[1]) - sborrow;
917 v128_t qdiff1 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[1], other.quad_data[1]), qborrow);
918 uint64_t sr1 = static_cast<uint64_t>(sdiff1) & MASK;
919 v128_t qr1 = wasm_v128_and(qdiff1, mask_splat);
920 sborrow = (sdiff1 < 0) ? 1 : 0;
921 qborrow = wasm_u32x4_shr(qdiff1, 31);
922 vector_field_detail::bb_vf_barrier_sq(sr1, qr1);
923 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
924
925 int64_t sdiff2 = static_cast<int64_t>(scalar_data[2]) - static_cast<int64_t>(other.scalar_data[2]) - sborrow;
926 v128_t qdiff2 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[2], other.quad_data[2]), qborrow);
927 uint64_t sr2 = static_cast<uint64_t>(sdiff2) & MASK;
928 v128_t qr2 = wasm_v128_and(qdiff2, mask_splat);
929 sborrow = (sdiff2 < 0) ? 1 : 0;
930 qborrow = wasm_u32x4_shr(qdiff2, 31);
931 vector_field_detail::bb_vf_barrier_sq(sr2, qr2);
932 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
933
934 int64_t sdiff3 = static_cast<int64_t>(scalar_data[3]) - static_cast<int64_t>(other.scalar_data[3]) - sborrow;
935 v128_t qdiff3 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[3], other.quad_data[3]), qborrow);
936 uint64_t sr3 = static_cast<uint64_t>(sdiff3) & MASK;
937 v128_t qr3 = wasm_v128_and(qdiff3, mask_splat);
938 sborrow = (sdiff3 < 0) ? 1 : 0;
939 qborrow = wasm_u32x4_shr(qdiff3, 31);
940 vector_field_detail::bb_vf_barrier_sq(sr3, qr3);
941 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
942
943 int64_t sdiff4 = static_cast<int64_t>(scalar_data[4]) - static_cast<int64_t>(other.scalar_data[4]) - sborrow;
944 v128_t qdiff4 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[4], other.quad_data[4]), qborrow);
945 uint64_t sr4 = static_cast<uint64_t>(sdiff4) & MASK;
946 v128_t qr4 = wasm_v128_and(qdiff4, mask_splat);
947 sborrow = (sdiff4 < 0) ? 1 : 0;
948 qborrow = wasm_u32x4_shr(qdiff4, 31);
949 vector_field_detail::bb_vf_barrier_sq(sr4, qr4);
950 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
951
952 int64_t sdiff5 = static_cast<int64_t>(scalar_data[5]) - static_cast<int64_t>(other.scalar_data[5]) - sborrow;
953 v128_t qdiff5 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[5], other.quad_data[5]), qborrow);
954 uint64_t sr5 = static_cast<uint64_t>(sdiff5) & MASK;
955 v128_t qr5 = wasm_v128_and(qdiff5, mask_splat);
956 sborrow = (sdiff5 < 0) ? 1 : 0;
957 qborrow = wasm_u32x4_shr(qdiff5, 31);
958 vector_field_detail::bb_vf_barrier_sq(sr5, qr5);
959 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
960
961 int64_t sdiff6 = static_cast<int64_t>(scalar_data[6]) - static_cast<int64_t>(other.scalar_data[6]) - sborrow;
962 v128_t qdiff6 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[6], other.quad_data[6]), qborrow);
963 uint64_t sr6 = static_cast<uint64_t>(sdiff6) & MASK;
964 v128_t qr6 = wasm_v128_and(qdiff6, mask_splat);
965 sborrow = (sdiff6 < 0) ? 1 : 0;
966 qborrow = wasm_u32x4_shr(qdiff6, 31);
967 vector_field_detail::bb_vf_barrier_sq(sr6, qr6);
968 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
969
970 int64_t sdiff7 = static_cast<int64_t>(scalar_data[7]) - static_cast<int64_t>(other.scalar_data[7]) - sborrow;
971 v128_t qdiff7 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[7], other.quad_data[7]), qborrow);
972 uint64_t sr7 = static_cast<uint64_t>(sdiff7) & MASK;
973 v128_t qr7 = wasm_v128_and(qdiff7, mask_splat);
974 sborrow = (sdiff7 < 0) ? 1 : 0;
975 qborrow = wasm_u32x4_shr(qdiff7, 31);
976 vector_field_detail::bb_vf_barrier_sq(sr7, qr7);
977 asm volatile("" : "+r"(sborrow), "+r"(qborrow));
978
979 int64_t sdiff8 = static_cast<int64_t>(scalar_data[8]) - static_cast<int64_t>(other.scalar_data[8]) - sborrow;
980 v128_t qdiff8 = wasm_i32x4_sub(wasm_i32x4_sub(quad_data[8], other.quad_data[8]), qborrow);
981 uint64_t sr8 = static_cast<uint64_t>(sdiff8) & MASK;
982 v128_t qr8 = wasm_v128_and(qdiff8, mask_splat);
983 // Final borrow — this is what decides whether to add 2p.
984 const uint64_t s_final_borrow = (sdiff8 < 0) ? 1 : 0;
985 const v128_t q_final_borrow_i32 = wasm_u32x4_shr(qdiff8, 31); // 0 or 1 per lane
986 // q_final_borrow_mask: all-ones per lane if borrow set, else 0.
987 const v128_t q_final_borrow_mask = wasm_i32x4_eq(q_final_borrow_i32, wasm_i32x4_splat(1));
988
989 // s = r + 2p chain (scalar + quad interleaved).
990 uint64_t ss0 = sr0 + TWOP_WASM[0];
991 v128_t qs0 = wasm_i32x4_add(qr0, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[0])));
992 uint64_t scarry = ss0 >> 29;
993 v128_t qcarry = wasm_u32x4_shr(qs0, 29);
994 ss0 &= MASK;
995 qs0 = wasm_v128_and(qs0, mask_splat);
996 vector_field_detail::bb_vf_barrier_sqq(ss0, qs0, qcarry);
997 asm volatile("" : "+r"(scarry));
998
999 uint64_t ss1 = sr1 + TWOP_WASM[1] + scarry;
1000 v128_t qs1 = wasm_i32x4_add(wasm_i32x4_add(qr1, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[1]))), qcarry);
1001 scarry = ss1 >> 29;
1002 qcarry = wasm_u32x4_shr(qs1, 29);
1003 ss1 &= MASK;
1004 qs1 = wasm_v128_and(qs1, mask_splat);
1005 vector_field_detail::bb_vf_barrier_sqq(ss1, qs1, qcarry);
1006 asm volatile("" : "+r"(scarry));
1007
1008 uint64_t ss2 = sr2 + TWOP_WASM[2] + scarry;
1009 v128_t qs2 = wasm_i32x4_add(wasm_i32x4_add(qr2, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[2]))), qcarry);
1010 scarry = ss2 >> 29;
1011 qcarry = wasm_u32x4_shr(qs2, 29);
1012 ss2 &= MASK;
1013 qs2 = wasm_v128_and(qs2, mask_splat);
1014 vector_field_detail::bb_vf_barrier_sqq(ss2, qs2, qcarry);
1015 asm volatile("" : "+r"(scarry));
1016
1017 uint64_t ss3 = sr3 + TWOP_WASM[3] + scarry;
1018 v128_t qs3 = wasm_i32x4_add(wasm_i32x4_add(qr3, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[3]))), qcarry);
1019 scarry = ss3 >> 29;
1020 qcarry = wasm_u32x4_shr(qs3, 29);
1021 ss3 &= MASK;
1022 qs3 = wasm_v128_and(qs3, mask_splat);
1023 vector_field_detail::bb_vf_barrier_sqq(ss3, qs3, qcarry);
1024 asm volatile("" : "+r"(scarry));
1025
1026 uint64_t ss4 = sr4 + TWOP_WASM[4] + scarry;
1027 v128_t qs4 = wasm_i32x4_add(wasm_i32x4_add(qr4, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[4]))), qcarry);
1028 scarry = ss4 >> 29;
1029 qcarry = wasm_u32x4_shr(qs4, 29);
1030 ss4 &= MASK;
1031 qs4 = wasm_v128_and(qs4, mask_splat);
1032 vector_field_detail::bb_vf_barrier_sqq(ss4, qs4, qcarry);
1033 asm volatile("" : "+r"(scarry));
1034
1035 uint64_t ss5 = sr5 + TWOP_WASM[5] + scarry;
1036 v128_t qs5 = wasm_i32x4_add(wasm_i32x4_add(qr5, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[5]))), qcarry);
1037 scarry = ss5 >> 29;
1038 qcarry = wasm_u32x4_shr(qs5, 29);
1039 ss5 &= MASK;
1040 qs5 = wasm_v128_and(qs5, mask_splat);
1041 vector_field_detail::bb_vf_barrier_sqq(ss5, qs5, qcarry);
1042 asm volatile("" : "+r"(scarry));
1043
1044 uint64_t ss6 = sr6 + TWOP_WASM[6] + scarry;
1045 v128_t qs6 = wasm_i32x4_add(wasm_i32x4_add(qr6, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[6]))), qcarry);
1046 scarry = ss6 >> 29;
1047 qcarry = wasm_u32x4_shr(qs6, 29);
1048 ss6 &= MASK;
1049 qs6 = wasm_v128_and(qs6, mask_splat);
1050 vector_field_detail::bb_vf_barrier_sqq(ss6, qs6, qcarry);
1051 asm volatile("" : "+r"(scarry));
1052
1053 uint64_t ss7 = sr7 + TWOP_WASM[7] + scarry;
1054 v128_t qs7 = wasm_i32x4_add(wasm_i32x4_add(qr7, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[7]))), qcarry);
1055 scarry = ss7 >> 29;
1056 qcarry = wasm_u32x4_shr(qs7, 29);
1057 ss7 &= MASK;
1058 qs7 = wasm_v128_and(qs7, mask_splat);
1059 vector_field_detail::bb_vf_barrier_sqq(ss7, qs7, qcarry);
1060 asm volatile("" : "+r"(scarry));
1061
1062 uint64_t ss8 = sr8 + TWOP_WASM[8] + scarry;
1063 v128_t qs8 = wasm_i32x4_add(wasm_i32x4_add(qr8, wasm_i32x4_splat(static_cast<int32_t>(TWOP_WASM[8]))), qcarry);
1064 ss8 &= MASK;
1065 qs8 = wasm_v128_and(qs8, mask_splat);
1066
1067 // Blend on final borrow: borrow set => pick s.
1068 const uint64_t smask = 0ULL - s_final_borrow;
1069 const uint64_t simask = ~smask;
1070 const v128_t qmask = q_final_borrow_mask;
1071
1072 result.scalar_data[0] = static_cast<uint32_t>((sr0 & simask) | (ss0 & smask));
1073 result.quad_data[0] = wasm_v128_bitselect(qs0, qr0, qmask);
1074 result.scalar_data[1] = static_cast<uint32_t>((sr1 & simask) | (ss1 & smask));
1075 result.quad_data[1] = wasm_v128_bitselect(qs1, qr1, qmask);
1076 result.scalar_data[2] = static_cast<uint32_t>((sr2 & simask) | (ss2 & smask));
1077 result.quad_data[2] = wasm_v128_bitselect(qs2, qr2, qmask);
1078 result.scalar_data[3] = static_cast<uint32_t>((sr3 & simask) | (ss3 & smask));
1079 result.quad_data[3] = wasm_v128_bitselect(qs3, qr3, qmask);
1080 result.scalar_data[4] = static_cast<uint32_t>((sr4 & simask) | (ss4 & smask));
1081 result.quad_data[4] = wasm_v128_bitselect(qs4, qr4, qmask);
1082 result.scalar_data[5] = static_cast<uint32_t>((sr5 & simask) | (ss5 & smask));
1083 result.quad_data[5] = wasm_v128_bitselect(qs5, qr5, qmask);
1084 result.scalar_data[6] = static_cast<uint32_t>((sr6 & simask) | (ss6 & smask));
1085 result.quad_data[6] = wasm_v128_bitselect(qs6, qr6, qmask);
1086 result.scalar_data[7] = static_cast<uint32_t>((sr7 & simask) | (ss7 & smask));
1087 result.quad_data[7] = wasm_v128_bitselect(qs7, qr7, qmask);
1088 result.scalar_data[8] = static_cast<uint32_t>((sr8 & simask) | (ss8 & smask));
1089 result.quad_data[8] = wasm_v128_bitselect(qs8, qr8, qmask);
1090
1091 return result;
1092}
1093
1094// -------------------- eq / is_zero (coarse form, 9x29 limbs) --------------------
1095//
1096// Coarse-form equality trick: two elements are equal iff their difference d
1097// satisfies d == 0 or d == p (in 9 x 29-bit form). We compute d = a - b and
1098// OR-reduce the limbs both as-is (for d == 0) and XOR'd with p (for d == p).
1099//
1100// To avoid slow per-lane extract_lane calls, we use wasm_i32x4_bitmask to turn
1101// a 4-lane all-ones/zero compare into a 4-bit integer mask in one instruction.
1102
1103template <class Params>
1104[[gnu::always_inline]] inline uint32_t VectorField<Params>::eq_mask(const VectorField& other) const noexcept
1105{
1106 const VectorField d = (*this) - other;
1107
1108 // Scalar + quad OR-reductions interleaved. Two parallel accumulators per
1109 // stream: one for (d == 0), one for (d ^ p == 0 i.e. d == p).
1110 uint64_t sacc_z = d.scalar_data[0];
1111 v128_t qacc_z = d.quad_data[0];
1112 uint64_t sacc_p = d.scalar_data[0] ^ P_WASM[0];
1113 v128_t qacc_p = wasm_v128_xor(d.quad_data[0], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[0])));
1114 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1115 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1116
1117 sacc_z |= d.scalar_data[1];
1118 qacc_z = wasm_v128_or(qacc_z, d.quad_data[1]);
1119 sacc_p |= d.scalar_data[1] ^ P_WASM[1];
1120 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[1], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[1]))));
1121 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1122 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1123
1124 sacc_z |= d.scalar_data[2];
1125 qacc_z = wasm_v128_or(qacc_z, d.quad_data[2]);
1126 sacc_p |= d.scalar_data[2] ^ P_WASM[2];
1127 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[2], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[2]))));
1128 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1129 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1130
1131 sacc_z |= d.scalar_data[3];
1132 qacc_z = wasm_v128_or(qacc_z, d.quad_data[3]);
1133 sacc_p |= d.scalar_data[3] ^ P_WASM[3];
1134 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[3], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[3]))));
1135 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1136 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1137
1138 sacc_z |= d.scalar_data[4];
1139 qacc_z = wasm_v128_or(qacc_z, d.quad_data[4]);
1140 sacc_p |= d.scalar_data[4] ^ P_WASM[4];
1141 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[4], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[4]))));
1142 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1143 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1144
1145 sacc_z |= d.scalar_data[5];
1146 qacc_z = wasm_v128_or(qacc_z, d.quad_data[5]);
1147 sacc_p |= d.scalar_data[5] ^ P_WASM[5];
1148 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[5], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[5]))));
1149 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1150 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1151
1152 sacc_z |= d.scalar_data[6];
1153 qacc_z = wasm_v128_or(qacc_z, d.quad_data[6]);
1154 sacc_p |= d.scalar_data[6] ^ P_WASM[6];
1155 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[6], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[6]))));
1156 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1157 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1158
1159 sacc_z |= d.scalar_data[7];
1160 qacc_z = wasm_v128_or(qacc_z, d.quad_data[7]);
1161 sacc_p |= d.scalar_data[7] ^ P_WASM[7];
1162 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[7], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[7]))));
1163 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1164 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1165
1166 sacc_z |= d.scalar_data[8];
1167 qacc_z = wasm_v128_or(qacc_z, d.quad_data[8]);
1168 sacc_p |= d.scalar_data[8] ^ P_WASM[8];
1169 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(d.quad_data[8], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[8]))));
1170 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1171 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1172
1173 // For the quad stream, we want a 4-bit lane-equal mask. bitmask extracts
1174 // the top bit of each i32 lane. To make "acc == 0" produce top-bit-set,
1175 // compare to 0 with i32x4_eq → all-ones/0 per lane → bitmask extracts.
1176 // One i32x4_eq + one bitmask per accumulator — much cheaper than 4 extracts.
1177 const v128_t qzero = wasm_i32x4_splat(0);
1178 const uint32_t lanes_z = wasm_i32x4_bitmask(wasm_i32x4_eq(qacc_z, qzero));
1179 const uint32_t lanes_p = wasm_i32x4_bitmask(wasm_i32x4_eq(qacc_p, qzero));
1180 const uint32_t lanes_eq = lanes_z | lanes_p; // bits 0..3 per lane
1181
1182 const uint32_t scalar_eq = ((sacc_z == 0) || (sacc_p == 0)) ? 1u : 0u;
1183
1184 // Result: bit 0 = scalar, bits 1..4 = lanes 0..3. Shift lanes_eq left by 1.
1185 return scalar_eq | (lanes_eq << 1);
1186}
1187
1188template <class Params> [[gnu::always_inline]] inline uint32_t VectorField<Params>::is_zero_mask() const noexcept
1189{
1190 // Same pattern as eq, but on (*this) directly (no subtract).
1191 uint64_t sacc_z = scalar_data[0];
1192 v128_t qacc_z = quad_data[0];
1193 uint64_t sacc_p = scalar_data[0] ^ P_WASM[0];
1194 v128_t qacc_p = wasm_v128_xor(quad_data[0], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[0])));
1195 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1196 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1197
1198 sacc_z |= scalar_data[1];
1199 qacc_z = wasm_v128_or(qacc_z, quad_data[1]);
1200 sacc_p |= scalar_data[1] ^ P_WASM[1];
1201 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[1], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[1]))));
1202 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1203 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1204
1205 sacc_z |= scalar_data[2];
1206 qacc_z = wasm_v128_or(qacc_z, quad_data[2]);
1207 sacc_p |= scalar_data[2] ^ P_WASM[2];
1208 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[2], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[2]))));
1209 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1210 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1211
1212 sacc_z |= scalar_data[3];
1213 qacc_z = wasm_v128_or(qacc_z, quad_data[3]);
1214 sacc_p |= scalar_data[3] ^ P_WASM[3];
1215 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[3], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[3]))));
1216 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1217 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1218
1219 sacc_z |= scalar_data[4];
1220 qacc_z = wasm_v128_or(qacc_z, quad_data[4]);
1221 sacc_p |= scalar_data[4] ^ P_WASM[4];
1222 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[4], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[4]))));
1223 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1224 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1225
1226 sacc_z |= scalar_data[5];
1227 qacc_z = wasm_v128_or(qacc_z, quad_data[5]);
1228 sacc_p |= scalar_data[5] ^ P_WASM[5];
1229 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[5], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[5]))));
1230 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1231 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1232
1233 sacc_z |= scalar_data[6];
1234 qacc_z = wasm_v128_or(qacc_z, quad_data[6]);
1235 sacc_p |= scalar_data[6] ^ P_WASM[6];
1236 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[6], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[6]))));
1237 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1238 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1239
1240 sacc_z |= scalar_data[7];
1241 qacc_z = wasm_v128_or(qacc_z, quad_data[7]);
1242 sacc_p |= scalar_data[7] ^ P_WASM[7];
1243 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[7], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[7]))));
1244 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1245 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1246
1247 sacc_z |= scalar_data[8];
1248 qacc_z = wasm_v128_or(qacc_z, quad_data[8]);
1249 sacc_p |= scalar_data[8] ^ P_WASM[8];
1250 qacc_p = wasm_v128_or(qacc_p, wasm_v128_xor(quad_data[8], wasm_i32x4_splat(static_cast<int32_t>(P_WASM[8]))));
1251 vector_field_detail::bb_vf_barrier_sq(sacc_z, qacc_z);
1252 vector_field_detail::bb_vf_barrier_sq(sacc_p, qacc_p);
1253
1254 const v128_t qzero = wasm_i32x4_splat(0);
1255 const uint32_t lanes_z = wasm_i32x4_bitmask(wasm_i32x4_eq(qacc_z, qzero));
1256 const uint32_t lanes_p = wasm_i32x4_bitmask(wasm_i32x4_eq(qacc_p, qzero));
1257 const uint32_t lanes_iz = lanes_z | lanes_p;
1258
1259 const uint32_t scalar_iz = ((sacc_z == 0) || (sacc_p == 0)) ? 1u : 0u;
1260
1261 return scalar_iz | (lanes_iz << 1);
1262}
1263
1264// -------------------- operator* (Mont-mul via Karatsuba, 9x29 limbs) --------------------
1265//
1266// Follows the authoritative OPS[] schedule from
1267// https://gist.github.com/AztecBot/b8e2e1d5c85d54e10fb34b48461361e0.
1268//
1269// Stages:
1270// 1. P_lo = left[0..4] * right[0..4] (25 muls in pl0..pl8)
1271// 2. P_hi = left[5..8] * right[5..8] (16 muls in ph0..ph6)
1272// 3. Sums sl_i = l_i + l_{5+i} for i=0..3, sl4 = l4, same for sr (i32x4 add)
1273// 4. P_cross= sl * sr (25 muls in pc0..pc8)
1274// 5. Combine into temp_0..temp_16
1275// 6. 8 x Yuval reductions over (temp_lo..temp_{lo+9})
1276// 7. 1 x wasm_reduce over (temp_8..temp_16)
1277// 8. Carry-propagate temp_9..temp_17
1278// 9. Branch-free conditional subtract
1279// 10. Store output
1280//
1281// Scalar stream uses i64 arithmetic (29*29 -> 58-bit products fit in u64 with
1282// 6 bits of accumulator headroom for 9 partial products).
1283//
1284// Quad stream uses paired i64x2 accumulators: lane L holds field L's limb k.
1285// Two i64x2 slots per logical limb cover 4 fields (tlo = lanes 0/1 = fields
1286// 0,1; thi = lanes 2/3 = fields 2,3). Partial products use
1287// `i64x2.extmul_low/high_u32x4` to do 2 x (32x32->64) per v128 op.
1288//
1289// Source order: scalar op, then equivalent quad op(s), then next scalar op, etc.
1290// Clang preserves source order in WASM codegen; V8 sees independent scalar /
1291// SIMD ops and schedules them on separate pipes.
1292//
1293// Total muls in the product phase: 25 + 16 + 25 = 66 (NOT 81). This is the
1294// whole point of Karatsuba — schoolbook 9x9 would need 81.
1295
1296// The body of VectorField<Bn254FrParams>::operator* lives out-of-line in
1297// vector_field_wasm.cpp (see header there for the TU-boundary rationale).
1298// The primary template below has no body in the SIMD path; new Params
1299// specializations must be added there too. The non-SIMD fallback uses the
1300// generic template below.
1301
1302#else // !BB_VECTOR_FIELD_SIMD
1303
1304// ======================== Portable fallback ========================
1305// No SIMD: store 5 fields side-by-side, apply scalar field ops one at a time.
1306// Used on native x86/ARM builds.
1307
1308template <class Params> inline void VectorField<Params>::store_from_array(const std::array<Field, 5>& in) noexcept
1309{
1310 for (size_t i = 0; i < 5; ++i) {
1311 elts[i] = in[i];
1312 }
1313}
1314
1315template <class Params> inline void VectorField<Params>::load_to_array(std::array<Field, 5>& out) const noexcept
1316{
1317 for (size_t i = 0; i < 5; ++i) {
1318 out[i] = elts[i];
1319 }
1320}
1321
1322template <class Params> inline VectorField<Params>::VectorField(const Field* base) noexcept
1323{
1324 for (size_t i = 0; i < 5; ++i) {
1325 elts[i] = base[i];
1326 }
1327}
1328
1329template <class Params> inline VectorField<Params> VectorField<Params>::load_contiguous(const Field* base) noexcept
1330{
1331 return VectorField(base);
1332}
1333
1334template <class Params> inline void VectorField<Params>::store_contiguous(Field* base) const noexcept
1335{
1336 for (size_t i = 0; i < 5; ++i) {
1337 base[i] = elts[i];
1338 }
1339}
1340
1341template <class Params> inline void VectorField<Params>::store_to(Field* base) const noexcept
1342{
1343 store_contiguous(base);
1344}
1345
1346template <class Params> inline VectorField<Params> VectorField<Params>::broadcast(const Field& s) noexcept
1347{
1348 VectorField r;
1349 for (size_t i = 0; i < 5; ++i) {
1350 r.elts[i] = s;
1351 }
1352 return r;
1353}
1354
1355template <class Params>
1357{
1358 VectorField r;
1359 for (size_t i = 0; i < 5; ++i) {
1360 r.elts[i] = elts[i] + other.elts[i];
1361 }
1362 return r;
1363}
1364
1365template <class Params>
1367{
1368 VectorField r;
1369 for (size_t i = 0; i < 5; ++i) {
1370 r.elts[i] = elts[i] - other.elts[i];
1371 }
1372 return r;
1373}
1374
1375template <class Params>
1377{
1378 VectorField r;
1379 for (size_t i = 0; i < 5; ++i) {
1380 r.elts[i] = elts[i] * other.elts[i];
1381 }
1382 return r;
1383}
1384
1385template <class Params> inline uint32_t VectorField<Params>::eq_mask(const VectorField& other) const noexcept
1386{
1387 uint32_t m = 0;
1388 for (size_t i = 0; i < 5; ++i) {
1389 if (elts[i] == other.elts[i]) {
1390 m |= (1u << i);
1391 }
1392 }
1393 return m;
1394}
1395
1396template <class Params> inline uint32_t VectorField<Params>::is_zero_mask() const noexcept
1397{
1398 uint32_t m = 0;
1399 for (size_t i = 0; i < 5; ++i) {
1400 if (elts[i].is_zero()) {
1401 m |= (1u << i);
1402 }
1403 }
1404 return m;
1405}
1406
1407#endif // BB_VECTOR_FIELD_SIMD
1408
1409} // namespace bb
Parameters defining the base field of the BN254 curve.
Definition fq.hpp:28
Parameters defining the scalar field of the BN254 curve.
Definition fr.hpp:29
FF a
ssize_t offset
Definition engine.cpp:62
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
constexpr std::array< uint64_t, 9 > compute_twice_modulus_wasm() noexcept
constexpr bool has_simd_mont_mul_v
constexpr bool vector_field_simd_enabled
Inner sum(Cont< Inner, Args... > const &in)
Definition container.hpp:70
constexpr std::array< uint64_t, 9 > compute_tnm_wasm() noexcept
constexpr bool simd_available_v
STL namespace.
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::byte * data
bb::VectorAffineElementPushSpan< BaseParams > out
VectorField & operator-=(const VectorField &other) noexcept
VectorField(const Field &s) noexcept
static VectorField zero() noexcept
static constexpr std::array< uint64_t, 9 > TNM_WASM
VectorField invert() const noexcept
static constexpr std::array< uint64_t, 9 > P_WASM
friend VectorField operator+(const Field &s, VectorField v) noexcept
friend VectorField operator*(VectorField v, const Field &s) noexcept
VectorField & operator+=(const VectorField &other) noexcept
bool eq(const VectorField &other) const noexcept
constexpr VectorField() noexcept=default
VectorField(uint64_t s) noexcept
friend VectorField operator-(VectorField v, const Field &s) noexcept
Field get(size_t i) const noexcept
uint32_t is_zero_mask() const noexcept
friend VectorField operator+(VectorField v, const Field &s) noexcept
field< Params > Field
static constexpr auto modulus
void self_sqr() noexcept
VectorField(int s) noexcept
void load_to_array(std::array< Field, 5 > &out) const noexcept
static constexpr uint64_t R_INV_MOD_2_29
std::array< Field, 5 > to_array() const noexcept
bool is_zero() const noexcept
static VectorField from_lanes(const Fn &value_at) noexcept
VectorField(const Field *base) noexcept
static constexpr std::array< uint64_t, 9 > TWOP_WASM
uint32_t eq_mask(const VectorField &other) const noexcept
VectorField operator-(const VectorField &other) const noexcept
static VectorField load_contiguous(const Field *base) noexcept
static constexpr size_t SIZE
Field horizontal_sum() const noexcept
VectorField & operator*=(const VectorField &other) noexcept
VectorField operator+(const VectorField &other) const noexcept
static VectorField broadcast(const Field &s) noexcept
static VectorField gather(const Field *base, std::array< size_t, 5 > idx, size_t offset=0) noexcept
friend VectorField operator-(const Field &s, VectorField v) noexcept
void store_to(Field *base) const noexcept
VectorField operator*(const VectorField &other) const noexcept
void store_contiguous(Field *base) const noexcept
void scatter(Field *base, std::array< size_t, 5 > idx, size_t offset=0) const noexcept
VectorField sqr() const noexcept
static constexpr std::array< uint64_t, 9 > R_INV_WASM
friend VectorField operator*(const Field &s, VectorField v) noexcept
void set(size_t i, const Field &v) noexcept
void store_from_array(const std::array< Field, 5 > &in) noexcept
VectorField operator-() const noexcept
static VectorField one() noexcept
static constexpr field one()
static constexpr uint256_t modulus
static constexpr field zero()
#define BB_VECTOR_FIELD_SIMD
VectorField result