Skip to main content

vyre_primitives/decode/
ziftsieve.rs

1//! LZ4 sequence-index literal-copy primitive.
2//!
3//! LZ4-style formats have serial sequence discovery but parallel literal
4//! copying once an index exists. This primitive is the reusable second stage:
5//! one lane per sequence copies `[literal_start, literal_start + literal_len)`
6//! into the prefix-summed output offset. Producers may be CPU, CUDA, WGPU, or
7//! a future persistent decode megakernel as long as they satisfy the same
8//! sequence-index contract.
9
10use std::sync::Arc;
11
12use vyre_foundation::ir::model::expr::Ident;
13use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
14
15/// Canonical primitive op id.
16pub const OP_ID: &str = "vyre-primitives::decode::ziftsieve_literal_copy";
17/// One invocation processes one indexed LZ4 sequence.
18pub const WORKGROUP_SIZE: [u32; 3] = [64, 1, 1];
19/// Defensive upper bound for one compressed block.
20pub const MAX_BLOCK_SIZE: usize = 4 * 1024 * 1024;
21/// Defensive upper bound for sequence count in one block.
22pub const MAX_SEQUENCES_PER_BLOCK: usize = 100_000;
23
24/// Result of a reference LZ4 literal extraction.
25///
26/// `literals` holds the decoded bytes, CAPPED at the caller's `max_output`: the
27/// same fixed-output-buffer bound the GPU `ziftsieve_literal_copy` kernel enforces
28/// (it drops stores whose `literal_offset + i >= max_output`). `decoded_len` is the
29/// TRUE uncapped output length, the sum of every sequence's literal length, so a
30/// caller can detect a capped decode via [`ZiftsieveExtract::truncated`]. A bare
31/// `Vec<u8>` could not distinguish a complete decode from one silently truncated at
32/// the cap (a silent recall-loss gap, Law 10); the GPU path already exposes this
33/// host-side because the consumer builds the prefix-summed offsets and thus knows
34/// `offsets[last] + lens[last]` vs `max_output`. This mirrors [`super::inflate::CpuInflateResult`].
35#[cfg(any(test, feature = "cpu-parity"))]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ZiftsieveExtract {
38    /// Decoded literal bytes, capped at the caller's `max_output`.
39    pub literals: Vec<u8>,
40    /// True total decoded length across all sequences, BEFORE the `max_output` cap.
41    pub decoded_len: usize,
42}
43
44#[cfg(any(test, feature = "cpu-parity"))]
45impl ZiftsieveExtract {
46    /// True iff the decode was capped at `max_output` (bytes past the cap were
47    /// dropped). When true, `literals.len() == max_output < decoded_len`.
48    #[must_use]
49    pub fn truncated(&self) -> bool {
50        self.decoded_len > self.literals.len()
51    }
52}
53
54/// Host-side reference: sequential LZ4 literal extraction.
55///
56/// Returns the decoded bytes (capped at `max_output`) plus the true uncapped
57/// [`ZiftsieveExtract::decoded_len`], so a capped decode is observable rather than a
58/// silent recall loss (Law 10). Every malformed-input path still fails LOUD.
59///
60/// # Errors
61///
62/// Returns an actionable error string on malformed input. Every error message
63/// includes a `Fix:` tag.
64#[cfg(any(test, feature = "cpu-parity"))]
65pub fn ziftsieve_reference_extract_literals(
66    compressed: &[u8],
67    max_output: usize,
68) -> Result<ZiftsieveExtract, String> {
69    let initial_cap = compressed
70        .len()
71        .saturating_mul(2)
72        .min(max_output)
73        .min(MAX_BLOCK_SIZE);
74    let mut literals = Vec::with_capacity(initial_cap);
75    let mut decoded_len = 0usize;
76    let mut pos = 0usize;
77    let mut sequence_count = 0usize;
78
79    while pos < compressed.len() {
80        sequence_count += 1;
81        if sequence_count > MAX_SEQUENCES_PER_BLOCK {
82            return Err(format!(
83                "too many LZ4 sequences (max {MAX_SEQUENCES_PER_BLOCK}). \
84                 Fix: use a smaller LZ4 block or increase MAX_SEQUENCES_PER_BLOCK"
85            ));
86        }
87
88        let token = compressed[pos];
89        pos += 1;
90
91        let literal_len = (token >> 4) as usize;
92        let match_len = (token & 0x0F) as usize;
93
94        let literal_len = if literal_len == 15 {
95            decode_length(compressed, &mut pos, literal_len)?
96        } else {
97            literal_len
98        };
99
100        if literal_len > MAX_BLOCK_SIZE {
101            return Err(format!(
102                "literal length {literal_len} exceeds MAX_BLOCK_SIZE {MAX_BLOCK_SIZE}. \
103                 Fix: use a valid LZ4 stream"
104            ));
105        }
106
107        if pos + literal_len > compressed.len() {
108            return Err(format!(
109                "literal exceeds block bounds at offset {pos}. \
110                 Fix: use a valid LZ4 stream"
111            ));
112        }
113
114        // Count every valid in-stream literal toward the TRUE decoded length before
115        // applying the `max_output` cap, so the caller can detect a capped decode.
116        decoded_len = decoded_len.saturating_add(literal_len);
117        let remaining_output = max_output.saturating_sub(literals.len());
118        let to_copy = literal_len.min(remaining_output);
119        if to_copy > 0 {
120            literals.extend_from_slice(&compressed[pos..pos + to_copy]);
121        }
122        pos += literal_len;
123
124        if pos < compressed.len() {
125            if pos + 2 > compressed.len() {
126                return Err(format!(
127                    "truncated match offset at offset {pos}. \
128                     Fix: use a complete LZ4 stream"
129                ));
130            }
131            pos += 2;
132
133            if match_len == 15 {
134                let _match_len_extension = decode_length(compressed, &mut pos, match_len)?;
135            }
136        }
137    }
138
139    Ok(ZiftsieveExtract {
140        literals,
141        decoded_len,
142    })
143}
144
145fn decode_length(data: &[u8], pos: &mut usize, initial: usize) -> Result<usize, String> {
146    let mut len = initial;
147    loop {
148        if *pos >= data.len() {
149            return Err(format!(
150                "truncated length encoding at offset {pos}. \
151                 Fix: use a complete LZ4 stream"
152            ));
153        }
154        let byte = data[*pos];
155        *pos += 1;
156        len = len.checked_add(byte as usize).ok_or_else(|| {
157            "length overflow in variable-length encoding. Fix: use a valid LZ4 stream".to_string()
158        })?;
159        if byte < 255 {
160            break;
161        }
162        if len > MAX_BLOCK_SIZE {
163            return Err(format!(
164                "length {len} exceeds MAX_BLOCK_SIZE {MAX_BLOCK_SIZE}. \
165                 Fix: use a valid LZ4 stream"
166            ));
167        }
168    }
169    Ok(len)
170}
171
172/// Build the primitive body for indexed literal copy.
173#[must_use]
174pub fn ziftsieve_literal_copy_body(
175    input: &str,
176    output: &str,
177    seq_literal_start: &str,
178    seq_literal_len: &str,
179    seq_literal_offset: &str,
180    seq_count: u32,
181) -> Vec<Node> {
182    vec![
183        Node::let_bind("seq_idx", Expr::InvocationId { axis: 0 }),
184        Node::if_then(
185            Expr::lt(Expr::var("seq_idx"), Expr::u32(seq_count)),
186            vec![
187                Node::let_bind(
188                    "literal_start",
189                    Expr::load(seq_literal_start, Expr::var("seq_idx")),
190                ),
191                Node::let_bind(
192                    "literal_len",
193                    Expr::load(seq_literal_len, Expr::var("seq_idx")),
194                ),
195                Node::let_bind(
196                    "literal_offset",
197                    Expr::load(seq_literal_offset, Expr::var("seq_idx")),
198                ),
199                Node::loop_for(
200                    "i",
201                    Expr::u32(0),
202                    Expr::var("literal_len"),
203                    // Gate the data-derived copy on BOTH buffer bounds with control flow
204                    // (an `if_then`, NOT `Expr::select`: select still evaluates the OOB
205                    // load on a real GPU). The seq_* indices are unvalidated producer
206                    // input, so an out-of-contract `literal_start`/`literal_offset` would
207                    // otherwise be a raw OOB read (UB on CUDA) and OOB write (memory
208                    // corruption on CUDA). This puts the documented "drops stores whose
209                    // `literal_offset + i >= max_output`" cap INTO the IR instead of
210                    // relying on unreliable driver OOB behavior (see vyre-reference
211                    // oob.rs: "some clamp, some return zero, some crash"). Transparent to
212                    // every valid input (the producer contract keeps both indices in
213                    // bounds) and byte-identical to the interpreter's existing silent
214                    // OOB-store drop on a zero-initialized output.
215                    vec![Node::if_then(
216                        Expr::and(
217                            Expr::lt(
218                                Expr::add(Expr::var("literal_start"), Expr::var("i")),
219                                Expr::buf_len(input),
220                            ),
221                            Expr::lt(
222                                Expr::add(Expr::var("literal_offset"), Expr::var("i")),
223                                Expr::buf_len(output),
224                            ),
225                        ),
226                        vec![
227                            Node::let_bind(
228                                "src",
229                                Expr::load(
230                                    input,
231                                    Expr::add(Expr::var("literal_start"), Expr::var("i")),
232                                ),
233                            ),
234                            Node::store(
235                                output,
236                                Expr::add(Expr::var("literal_offset"), Expr::var("i")),
237                                Expr::var("src"),
238                            ),
239                        ],
240                    )],
241                ),
242            ],
243        ),
244    ]
245}
246
247/// Build a Program that copies indexed LZ4 literals in parallel.
248#[must_use]
249pub fn ziftsieve_literal_copy(
250    input: &str,
251    output: &str,
252    seq_literal_start: &str,
253    seq_literal_len: &str,
254    seq_literal_offset: &str,
255    input_len: u32,
256    seq_count: u32,
257    max_output: u32,
258) -> Program {
259    ziftsieve_literal_copy_with_op_id(
260        OP_ID,
261        input,
262        output,
263        seq_literal_start,
264        seq_literal_len,
265        seq_literal_offset,
266        input_len,
267        seq_count,
268        max_output,
269    )
270}
271
272/// Build a Program with a caller-provided op id.
273///
274/// Composition crates use this to preserve their public inventory id while
275/// reusing the primitive-owned IR builder.
276#[must_use]
277pub fn ziftsieve_literal_copy_with_op_id(
278    op_id: &str,
279    input: &str,
280    output: &str,
281    seq_literal_start: &str,
282    seq_literal_len: &str,
283    seq_literal_offset: &str,
284    input_len: u32,
285    seq_count: u32,
286    max_output: u32,
287) -> Program {
288    let body = ziftsieve_literal_copy_body(
289        input,
290        output,
291        seq_literal_start,
292        seq_literal_len,
293        seq_literal_offset,
294        seq_count,
295    );
296
297    let input_decl = BufferDecl::storage(input, 0, BufferAccess::ReadOnly, DataType::U32);
298    let input_decl = if input_len == 0 {
299        input_decl
300    } else {
301        input_decl.with_count(input_len)
302    };
303
304    Program::wrapped(
305        vec![
306            input_decl,
307            BufferDecl::storage(seq_literal_start, 1, BufferAccess::ReadOnly, DataType::U32)
308                .with_count(seq_count.max(1)),
309            BufferDecl::storage(seq_literal_len, 2, BufferAccess::ReadOnly, DataType::U32)
310                .with_count(seq_count.max(1)),
311            BufferDecl::storage(seq_literal_offset, 3, BufferAccess::ReadOnly, DataType::U32)
312                .with_count(seq_count.max(1)),
313            BufferDecl::storage(output, 4, BufferAccess::ReadWrite, DataType::U32)
314                .with_count(max_output.max(1)),
315        ],
316        WORKGROUP_SIZE,
317        vec![Node::Region {
318            generator: Ident::from(op_id),
319            source_region: None,
320            body: Arc::new(body),
321        }],
322    )
323}
324
325#[cfg(feature = "inventory-registry")]
326fn fixture_inputs() -> Vec<Vec<Vec<u8>>> {
327    let input = crate::wire::pack_u32_slice(&[0x10, b'A' as u32, 0x20, b'B' as u32, b'C' as u32]);
328    let seq_literal_start = crate::wire::pack_u32_slice(&[1, 3]);
329    let seq_literal_len = crate::wire::pack_u32_slice(&[1, 2]);
330    let seq_literal_offset = crate::wire::pack_u32_slice(&[0, 1]);
331    vec![vec![
332        input,
333        seq_literal_start,
334        seq_literal_len,
335        seq_literal_offset,
336        vec![0u8; 3 * 4],
337    ]]
338}
339
340#[cfg(feature = "inventory-registry")]
341fn fixture_outputs() -> Vec<Vec<Vec<u8>>> {
342    vec![vec![crate::wire::pack_u32_slice(&[
343        b'A' as u32,
344        b'B' as u32,
345        b'C' as u32,
346    ])]]
347}
348
349#[cfg(feature = "inventory-registry")]
350inventory::submit! {
351    vyre_foundation::operation::OperationRegistration::primitive(
352        OP_ID,
353        || ziftsieve_literal_copy("input", "output", "seq_start", "seq_len", "seq_off", 5, 2, 3),
354        Some(fixture_inputs),
355        Some(fixture_outputs),
356    )
357}
358
359#[cfg(test)]
360#[allow(clippy::expect_used, clippy::unwrap_used)]
361mod tests {
362    use super::*;
363    use vyre_reference::value::Value;
364
365    fn run(input: &[u8], seq_starts: &[u32], seq_lens: &[u32], seq_offsets: &[u32]) -> Vec<u32> {
366        let seq_count = seq_starts.len() as u32;
367        let max_output = seq_lens.iter().copied().sum::<u32>();
368        let input_words = input.iter().map(|&b| u32::from(b)).collect::<Vec<_>>();
369        let program = ziftsieve_literal_copy(
370            "input",
371            "output",
372            "seq_start",
373            "seq_len",
374            "seq_off",
375            input.len() as u32,
376            seq_count,
377            max_output,
378        );
379        let inputs = vec![
380            Value::from(crate::wire::pack_u32_slice(&input_words)),
381            Value::from(crate::wire::pack_u32_slice(seq_starts)),
382            Value::from(crate::wire::pack_u32_slice(seq_lens)),
383            Value::from(crate::wire::pack_u32_slice(seq_offsets)),
384            Value::from(vec![0u8; (max_output.max(1) as usize) * 4]),
385        ];
386        let outputs = vyre_reference::reference_eval(&program, &inputs)
387            .expect("Fix: ziftsieve literal-copy primitive must run.");
388        let words = crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes());
389        words.into_iter().take(max_output as usize).collect()
390    }
391
392    #[test]
393    fn single_literal() {
394        assert_eq!(run(&[0x10, b'A'], &[1], &[1], &[0]), vec![b'A' as u32]);
395    }
396
397    #[test]
398    fn two_sequences() {
399        assert_eq!(
400            run(&[0x10, b'A', 0x20, b'B', b'C'], &[1, 3], &[1, 2], &[0, 1]),
401            vec![b'A' as u32, b'B' as u32, b'C' as u32]
402        );
403    }
404
405    #[test]
406    fn zero_literal_sequence_is_nop() {
407        assert_eq!(
408            run(&[0x00, 0x10, b'A'], &[0], &[0], &[0]),
409            Vec::<u32>::new()
410        );
411    }
412
413    #[test]
414    fn reference_extracts_simple_literal() {
415        let result = ziftsieve_reference_extract_literals(&[0x10, b'A'], 1024).unwrap();
416        assert_eq!(result.literals, b"A");
417        assert_eq!(result.decoded_len, 1);
418        assert!(!result.truncated());
419    }
420
421    #[test]
422    fn reference_extracts_with_match_skip() {
423        let data = [0x11, b'A', 0x01, 0x00];
424        let result = ziftsieve_reference_extract_literals(&data, 1024).unwrap();
425        assert_eq!(result.literals, b"A");
426        assert!(!result.truncated());
427    }
428
429    #[test]
430    fn reference_rejects_truncated_literal() {
431        let err = ziftsieve_reference_extract_literals(&[0x20, b'A'], 1024).unwrap_err();
432        assert!(err.contains("truncated") || err.contains("literal"));
433    }
434
435    #[test]
436    fn reference_accepts_exact_max_sequence_count() {
437        let mut data = Vec::new();
438        for _ in 1..MAX_SEQUENCES_PER_BLOCK {
439            data.push(0x10);
440            data.push(b'X');
441            data.extend_from_slice(&[0x00, 0x00]);
442        }
443        data.push(0x10);
444        data.push(b'X');
445
446        let result = ziftsieve_reference_extract_literals(&data, MAX_SEQUENCES_PER_BLOCK)
447            .expect("Fix: MAX_SEQUENCES_PER_BLOCK is an inclusive maximum, not an exclusive one.");
448        assert_eq!(result.literals.len(), MAX_SEQUENCES_PER_BLOCK);
449        assert!(result.literals.iter().all(|&byte| byte == b'X'));
450        assert!(!result.truncated());
451    }
452
453    #[test]
454    fn reference_rejects_too_many_sequences() {
455        let mut data = Vec::new();
456        for _ in 0..=MAX_SEQUENCES_PER_BLOCK {
457            data.push(0x10);
458            data.push(b'X');
459            data.extend_from_slice(&[0x00, 0x00]);
460        }
461        let err = ziftsieve_reference_extract_literals(&data, 1024).unwrap_err();
462        assert!(err.contains("sequence") || err.contains("MAX"));
463    }
464
465    /// Run the copy program with a caller-controlled output cap and a
466    /// SENTINEL-prefilled output buffer, returning the raw output words so a test
467    /// can prove which slots the gate left untouched. Unlike `run`, this does not
468    /// zero-init or truncate (it exposes the exact OOB behavior).
469    fn run_with_sentinel(
470        input_words: &[u32],
471        seq_starts: &[u32],
472        seq_lens: &[u32],
473        seq_offsets: &[u32],
474        max_output: u32,
475        sentinel: u32,
476    ) -> Vec<u32> {
477        let program = ziftsieve_literal_copy(
478            "input",
479            "output",
480            "seq_start",
481            "seq_len",
482            "seq_off",
483            input_words.len() as u32,
484            seq_starts.len() as u32,
485            max_output,
486        );
487        let inputs = vec![
488            Value::from(crate::wire::pack_u32_slice(input_words)),
489            Value::from(crate::wire::pack_u32_slice(seq_starts)),
490            Value::from(crate::wire::pack_u32_slice(seq_lens)),
491            Value::from(crate::wire::pack_u32_slice(seq_offsets)),
492            Value::from(crate::wire::pack_u32_slice(&vec![
493                sentinel;
494                max_output.max(1) as usize
495            ])),
496        ];
497        let outputs = vyre_reference::reference_eval(&program, &inputs)
498            .expect("Fix: out-of-contract ziftsieve copy must not fault the interpreter");
499        crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes())
500    }
501
502    #[test]
503    fn out_of_contract_offset_drops_stores_past_output_cap() {
504        // The seq_* indices are UNVALIDATED producer input. A literal whose copy
505        // runs past the `max_output` cap must have its out-of-range stores dropped
506        // BY THE IR gate (the documented contract), not by unreliable driver OOB
507        // behavior. Proven by a non-zero sentinel that survives past the cap and by
508        // the run not faulting. Buffer holds 3 slots; the sequence starts at offset
509        // 1 with length 4, so slots 3 and 4 are past the cap and must be dropped.
510        const SENTINEL: u32 = 0xDEAD_BEEF;
511        let words = run_with_sentinel(
512            &[b'A' as u32, b'B' as u32, b'C' as u32, b'D' as u32],
513            &[0],
514            &[4],
515            &[1],
516            3,
517            SENTINEL,
518        );
519        // slot 0: never written (offset starts at 1) → sentinel preserved.
520        // slots 1,2: in-bounds copies of input[0]=A, input[1]=B.
521        // slots 3,4: past the 3-slot cap → dropped (no panic, no corruption).
522        assert_eq!(
523            words,
524            vec![SENTINEL, b'A' as u32, b'B' as u32],
525            "Fix: stores past the output cap must be dropped by the IR gate, untouched slots keep their prior value"
526        );
527    }
528
529    #[test]
530    fn out_of_contract_literal_start_gates_oob_source_reads() {
531        // A `literal_start`/`literal_len` that runs past the input buffer must have
532        // its out-of-range SOURCE READS gated away entirely (no OOB load. UB on
533        // CUDA), leaving the corresponding output slots untouched. This distinguishes
534        // the control-flow gate from the OLD ungated IR: the old code zero-fill-loaded
535        // the OOB source and stored 0 (→ [B, 0, 0, SENTINEL]); the gate skips the whole
536        // iteration (→ [B, SENTINEL, SENTINEL, SENTINEL]).
537        const SENTINEL: u32 = 0x1234_5678;
538        let words = run_with_sentinel(
539            &[b'A' as u32, b'B' as u32], // input_len = 2
540            &[1],                        // start at the last valid index
541            &[3],                        // reads input[1] (ok), input[2],input[3] (OOB)
542            &[0],
543            4,
544            SENTINEL,
545        );
546        // i=0: input[1]=B → output[0]=B (both in bounds).
547        // i=1: input[2] OOB → iteration skipped → output[1] keeps sentinel.
548        // i=2: input[3] OOB → skipped → output[2] keeps sentinel.
549        // output[3]: never touched → sentinel.
550        assert_eq!(
551            words,
552            vec![b'B' as u32, SENTINEL, SENTINEL, SENTINEL],
553            "Fix: OOB source reads must be skipped by the IR gate (no OOB load), leaving output untouched"
554        );
555    }
556
557    #[test]
558    fn out_of_contract_copy_records_zero_interpreter_oob_accesses() {
559        // The whole point of the gate: on hostile input the program must NOT rely on
560        // the interpreter's silent OOB masking (zero-fill loads / dropped stores). A
561        // correctly-gated copy skips the out-of-range access with control flow, so
562        // reference_eval reports ZERO OOB accesses even though the sequence overshoots
563        // the 3-slot output. The pre-fix ungated store would OOB-write slots 3,4 past
564        // the buffer → nonzero, which is what a real GPU would corrupt.
565        let program = ziftsieve_literal_copy(
566            "input",
567            "output",
568            "seq_start",
569            "seq_len",
570            "seq_off",
571            4,
572            1,
573            3,
574        );
575        let (_outputs, report) = vyre_reference::reference_eval_oob_report(
576            &program,
577            &[
578                Value::from(crate::wire::pack_u32_slice(&[10, 20, 30, 40])),
579                Value::from(crate::wire::pack_u32_slice(&[0])), // literal_start
580                Value::from(crate::wire::pack_u32_slice(&[4])), // literal_len overshoots the cap
581                Value::from(crate::wire::pack_u32_slice(&[1])), // literal_offset → slots 1..4
582                Value::from(crate::wire::pack_u32_slice(&[0u32; 3])),
583            ],
584        )
585        .expect("Fix: ziftsieve copy must reference-evaluate");
586        assert_eq!(
587            report.total(),
588            0,
589            "Fix: the bounds-gated copy must never trigger interpreter OOB masking on hostile input"
590        );
591    }
592}