Skip to main content

vyre_libs/math/
succinct.rs

1//! Succinct bitvector metadata primitives.
2//!
3//! These ops build the rank side of rank/select navigation for compact token,
4//! AST, and graph bitvectors. They keep hot navigation state as packed `u32`
5//! words plus sparse superblock counters, so GPU kernels trade bandwidth-heavy
6//! pointer chasing for popcount math over coalesced words.
7
8use core::fmt;
9
10use crate::region::{tag_program, wrap_anonymous, wrap_child};
11use vyre::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
12use vyre_foundation::ir::model::expr::GeneratorRef;
13
14const RANK_SUPERBLOCKS_OP_ID: &str = "vyre-libs::math::succinct::rank1_superblocks";
15const RANK_QUERY_OP_ID: &str = "vyre-libs::math::succinct::rank1_query";
16const SELECT_QUERY_OP_ID: &str = "vyre-libs::math::succinct::select1_query";
17
18/// Build-time errors for succinct bitvector Programs.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum SuccinctBuildError {
21    /// Superblock size must be non-zero.
22    ZeroBlockWords,
23    /// The derived superblock output length overflowed `u32`.
24    SuperblockCountOverflow,
25}
26
27impl fmt::Display for SuccinctBuildError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::ZeroBlockWords => {
31                write!(f, "Fix: rank superblock size must be at least one u32 word")
32            }
33            Self::SuperblockCountOverflow => write!(
34                f,
35                "Fix: rank superblock count overflowed u32; shard the bitvector"
36            ),
37        }
38    }
39}
40
41impl std::error::Error for SuccinctBuildError {}
42
43fn superblock_count(word_count: u32, block_words: u32) -> Result<u32, SuccinctBuildError> {
44    if block_words == 0 {
45        return Err(SuccinctBuildError::ZeroBlockWords);
46    }
47    let full_blocks = word_count / block_words;
48    let has_partial = u32::from(word_count % block_words != 0);
49    full_blocks
50        .checked_add(has_partial)
51        .and_then(|blocks| blocks.checked_add(1))
52        .ok_or(SuccinctBuildError::SuperblockCountOverflow)
53}
54
55/// Build sparse rank1 superblocks for a packed u32 bitvector.
56///
57/// `superblocks[0]` is always zero. Each following entry stores the cumulative
58/// count of set bits before that superblock. The final sentinel stores the
59/// total popcount for the whole bitvector.
60#[must_use]
61pub fn rank1_superblocks(
62    bits: &str,
63    superblocks: &str,
64    word_count: u32,
65    block_words: u32,
66) -> Program {
67    try_rank1_superblocks(bits, superblocks, word_count, block_words).unwrap_or_else(|err| {
68        crate::builder::invalid_output_program(
69            RANK_SUPERBLOCKS_OP_ID,
70            superblocks,
71            DataType::U32,
72            format!("{err}"),
73        )
74    })
75}
76
77/// Checked builder for [`rank1_superblocks`].
78///
79/// # Errors
80///
81/// Returns [`SuccinctBuildError`] when `block_words` is zero or the derived
82/// metadata length overflows `u32`.
83pub fn try_rank1_superblocks(
84    bits: &str,
85    superblocks: &str,
86    word_count: u32,
87    block_words: u32,
88) -> Result<Program, SuccinctBuildError> {
89    let out_count = superblock_count(word_count, block_words)?;
90    let body = vec![Node::if_then(
91        Expr::eq(Expr::InvocationId { axis: 0 }, Expr::u32(0)),
92        vec![
93            Node::store(superblocks, Expr::u32(0), Expr::u32(0)),
94            Node::let_bind("rank_acc", Expr::u32(0)),
95            Node::loop_for(
96                "rank_word",
97                Expr::u32(0),
98                Expr::u32(word_count),
99                vec![
100                    Node::if_then(
101                        Expr::and(
102                            Expr::gt(Expr::var("rank_word"), Expr::u32(0)),
103                            Expr::eq(
104                                Expr::rem(Expr::var("rank_word"), Expr::u32(block_words)),
105                                Expr::u32(0),
106                            ),
107                        ),
108                        vec![Node::store(
109                            superblocks,
110                            Expr::div(Expr::var("rank_word"), Expr::u32(block_words)),
111                            Expr::var("rank_acc"),
112                        )],
113                    ),
114                    Node::assign(
115                        "rank_acc",
116                        Expr::add(
117                            Expr::var("rank_acc"),
118                            Expr::popcount(Expr::load(bits, Expr::var("rank_word"))),
119                        ),
120                    ),
121                ],
122            ),
123            Node::store(superblocks, Expr::u32(out_count - 1), Expr::var("rank_acc")),
124        ],
125    )];
126    Ok(Program::wrapped(
127        vec![
128            BufferDecl::storage(bits, 0, BufferAccess::ReadOnly, DataType::U32)
129                .with_count(word_count.max(1)),
130            BufferDecl::output(superblocks, 1, DataType::U32).with_count(out_count),
131        ],
132        [1, 1, 1],
133        vec![wrap_anonymous(
134            RANK_SUPERBLOCKS_OP_ID,
135            vec![wrap_child(
136                vyre_primitives::graph::path_reconstruct::OP_ID,
137                GeneratorRef {
138                    name: RANK_SUPERBLOCKS_OP_ID.to_string(),
139                },
140                body,
141            )],
142        )],
143    ))
144}
145
146/// Answer rank1-before-position queries from sparse superblocks.
147///
148/// Each `bit_indices[q]` is a zero-based bit offset. The output is the number
149/// of set bits strictly before that offset. Query offsets must address an
150/// existing packed word; use the final superblock sentinel for total popcount.
151#[must_use]
152pub fn rank1_query(
153    bits: &str,
154    superblocks: &str,
155    bit_indices: &str,
156    out: &str,
157    word_count: u32,
158    query_count: u32,
159    block_words: u32,
160) -> Program {
161    try_rank1_query(
162        bits,
163        superblocks,
164        bit_indices,
165        out,
166        word_count,
167        query_count,
168        block_words,
169    )
170    .unwrap_or_else(|err| {
171        crate::builder::invalid_output_program(
172            RANK_QUERY_OP_ID,
173            out,
174            DataType::U32,
175            format!("{err}"),
176        )
177    })
178}
179
180/// Checked builder for [`rank1_query`].
181///
182/// # Errors
183///
184/// Returns [`SuccinctBuildError`] when `block_words` is zero or the derived
185/// metadata length overflows `u32`.
186pub fn try_rank1_query(
187    bits: &str,
188    superblocks: &str,
189    bit_indices: &str,
190    out: &str,
191    word_count: u32,
192    query_count: u32,
193    block_words: u32,
194) -> Result<Program, SuccinctBuildError> {
195    let sb_count = superblock_count(word_count, block_words)?;
196    let q = Expr::InvocationId { axis: 0 };
197    let body = vec![Node::if_then(
198        Expr::lt(q.clone(), Expr::u32(query_count)),
199        vec![
200            Node::let_bind("bit_index", Expr::load(bit_indices, q.clone())),
201            Node::let_bind(
202                "word_index",
203                Expr::div(Expr::var("bit_index"), Expr::u32(32)),
204            ),
205            Node::if_then(
206                Expr::ge(Expr::var("word_index"), Expr::u32(word_count)),
207                vec![Node::trap(
208                    Expr::var("bit_index"),
209                    "rank-query-out-of-bounds",
210                )],
211            ),
212            Node::let_bind(
213                "block_index",
214                Expr::div(Expr::var("word_index"), Expr::u32(block_words)),
215            ),
216            Node::let_bind(
217                "rank_acc",
218                Expr::load(superblocks, Expr::var("block_index")),
219            ),
220            Node::let_bind(
221                "block_start_word",
222                Expr::mul(Expr::var("block_index"), Expr::u32(block_words)),
223            ),
224            Node::loop_for(
225                "rank_word",
226                Expr::var("block_start_word"),
227                Expr::var("word_index"),
228                vec![Node::assign(
229                    "rank_acc",
230                    Expr::add(
231                        Expr::var("rank_acc"),
232                        Expr::popcount(Expr::load(bits, Expr::var("rank_word"))),
233                    ),
234                )],
235            ),
236            Node::let_bind(
237                "bit_offset",
238                Expr::rem(Expr::var("bit_index"), Expr::u32(32)),
239            ),
240            Node::let_bind(
241                "partial_mask",
242                Expr::select(
243                    Expr::eq(Expr::var("bit_offset"), Expr::u32(0)),
244                    Expr::u32(0),
245                    Expr::sub(
246                        Expr::shl(Expr::u32(1), Expr::var("bit_offset")),
247                        Expr::u32(1),
248                    ),
249                ),
250            ),
251            Node::assign(
252                "rank_acc",
253                Expr::add(
254                    Expr::var("rank_acc"),
255                    Expr::popcount(Expr::bitand(
256                        Expr::load(bits, Expr::var("word_index")),
257                        Expr::var("partial_mask"),
258                    )),
259                ),
260            ),
261            Node::store(out, q, Expr::var("rank_acc")),
262        ],
263    )];
264    Ok(Program::wrapped(
265        vec![
266            BufferDecl::storage(bits, 0, BufferAccess::ReadOnly, DataType::U32)
267                .with_count(word_count.max(1)),
268            BufferDecl::storage(superblocks, 1, BufferAccess::ReadOnly, DataType::U32)
269                .with_count(sb_count),
270            BufferDecl::storage(bit_indices, 2, BufferAccess::ReadOnly, DataType::U32)
271                .with_count(query_count.max(1)),
272            BufferDecl::output(out, 3, DataType::U32).with_count(query_count.max(1)),
273        ],
274        [64, 1, 1],
275        vec![wrap_anonymous(RANK_QUERY_OP_ID, body)],
276    ))
277}
278
279/// Answer select1 queries over a packed u32 bitvector.
280///
281/// Each `k_indices[q]` is a one-based rank. The output is the zero-based bit
282/// position of the `k`-th set bit. `k == 0` and `k > total_popcount` trap
283/// loudly so callers cannot silently navigate to a bogus AST or graph node.
284#[must_use]
285pub fn select1_query(
286    bits: &str,
287    k_indices: &str,
288    out: &str,
289    word_count: u32,
290    query_count: u32,
291) -> Program {
292    try_select1_query(bits, k_indices, out, word_count, query_count).unwrap_or_else(|err| {
293        crate::builder::invalid_output_program(
294            SELECT_QUERY_OP_ID,
295            out,
296            DataType::U32,
297            format!("{err}"),
298        )
299    })
300}
301
302/// Checked builder for [`select1_query`].
303///
304/// # Errors
305///
306/// Currently this builder has no static failure modes. Runtime queries still
307/// trap when `k == 0` or when `k` exceeds the bitvector popcount.
308pub fn try_select1_query(
309    bits: &str,
310    k_indices: &str,
311    out: &str,
312    word_count: u32,
313    query_count: u32,
314) -> Result<Program, SuccinctBuildError> {
315    Ok(tag_program(
316        SELECT_QUERY_OP_ID,
317        vyre_primitives::bitset::select::select1_query(
318            bits,
319            k_indices,
320            out,
321            word_count,
322            query_count,
323        ),
324    ))
325}
326
327inventory::submit! {
328    crate::harness::OpEntry {
329        id: RANK_SUPERBLOCKS_OP_ID,
330        build: || rank1_superblocks("bits", "superblocks", 4, 2),
331        test_inputs: Some(|| {
332            let bits = [0b1011u32, 0x8000_0000, 0xFFFF_0000, 0u32];
333            let to_bytes = vyre_primitives::wire::pack_u32_slice;
334            vec![vec![to_bytes(&bits)]]
335        }),
336        expected_output: Some(|| {
337            let expected = [0u32, 4, 20];
338            let bytes = vyre_primitives::wire::pack_u32_slice(&expected);
339            vec![vec![bytes]]
340        }),
341        category: Some("math"),
342    }
343}
344
345inventory::submit! {
346    crate::harness::OpEntry {
347        id: SELECT_QUERY_OP_ID,
348        build: || select1_query("bits", "queries", "out", 4, 5),
349        test_inputs: Some(|| {
350            let bits = [0b1011u32, 0x8000_0000, 0xFFFF_0000, 0u32];
351            let queries = [1u32, 2, 3, 4, 5];
352            let to_bytes = vyre_primitives::wire::pack_u32_slice;
353            vec![vec![to_bytes(&bits), to_bytes(&queries)]]
354        }),
355        expected_output: Some(|| {
356            let expected = [0u32, 1, 3, 63, 80];
357            let bytes = vyre_primitives::wire::pack_u32_slice(&expected);
358            vec![vec![bytes]]
359        }),
360        category: Some("math"),
361    }
362}
363
364inventory::submit! {
365    crate::harness::OpEntry {
366        id: RANK_QUERY_OP_ID,
367        build: || rank1_query("bits", "superblocks", "queries", "out", 4, 5, 2),
368        test_inputs: Some(|| {
369            let bits = [0b1011u32, 0x8000_0000, 0xFFFF_0000, 0u32];
370            let superblocks = [0u32, 4, 20];
371            let queries = [0u32, 1, 4, 63, 80];
372            let to_bytes = vyre_primitives::wire::pack_u32_slice;
373            vec![vec![to_bytes(&bits), to_bytes(&superblocks), to_bytes(&queries)]]
374        }),
375        expected_output: Some(|| {
376            let expected = [0u32, 1, 3, 3, 4];
377            let bytes = vyre_primitives::wire::pack_u32_slice(&expected);
378            vec![vec![bytes]]
379        }),
380        category: Some("math"),
381    }
382}