Skip to main content

hekate_program/
expander.rs

1// SPDX-License-Identifier: Apache-2.0
2// This file is part of the hekate project.
3// Copyright (C) 2026 Andrei Kochergin <andrei@oumuamua.dev>
4// Copyright (C) 2026 Oumuamua Labs <info@oumuamua.dev>.
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use alloc::vec;
19use alloc::vec::Vec;
20use core::iter::repeat_n;
21use hekate_core::config::Config;
22use hekate_core::errors::Error;
23use hekate_core::poly::PolyVariant;
24use hekate_core::trace::{ColumnType, Trace, TraceColumn, TraceCompatibleField};
25use hekate_core::utils::compute_split_vars;
26use hekate_math::{Bit, Block8, Block16, Block32, Block64, Flat, HardwareField};
27
28/// Serializable expansion step descriptor.
29#[derive(Clone, Copy, Debug)]
30pub enum ExpansionEntry {
31    ExpandBits {
32        count: usize,
33        storage: ColumnType,
34    },
35    PassThrough {
36        count: usize,
37        storage: ColumnType,
38    },
39    ControlBits {
40        count: usize,
41    },
42    ReusePassThrough {
43        phy_col_start: usize,
44        count: usize,
45        storage: ColumnType,
46    },
47    ReuseExpandBits {
48        phy_col_start: usize,
49        count: usize,
50        storage: ColumnType,
51    },
52}
53
54/// Physical-to-virtual column mapping rule.
55#[derive(Clone, Copy, Debug)]
56enum EntryKind {
57    /// N physical columns to N ×
58    /// bit_width virtual Bit columns.
59    ExpandBits { count: usize, storage: ColumnType },
60
61    /// N physical columns to N virtual
62    /// columns of the same type.
63    PassThrough { count: usize, storage: ColumnType },
64
65    /// N physical Bit columns
66    /// to N virtual Bit columns.
67    ControlBits { count: usize },
68}
69
70impl EntryKind {
71    fn count(&self) -> usize {
72        match self {
73            Self::ExpandBits { count, .. }
74            | Self::PassThrough { count, .. }
75            | Self::ControlBits { count } => *count,
76        }
77    }
78
79    fn storage(&self) -> ColumnType {
80        match self {
81            Self::ExpandBits { storage, .. } | Self::PassThrough { storage, .. } => *storage,
82            Self::ControlBits { .. } => ColumnType::Bit,
83        }
84    }
85}
86
87/// Pre-computed expansion entry
88/// with frozen byte/column offsets.
89#[derive(Clone, Copy, Debug)]
90struct CompiledEntry {
91    /// Physical column index,
92    /// relative to `phy_start_idx`.
93    phy_col_start: usize,
94
95    /// Byte offset in the committed row.
96    byte_offset: usize,
97    kind: EntryKind,
98
99    /// True if this entry reuses physical
100    /// columns declared by a prior entry.
101    reuse: bool,
102}
103
104/// Declarative physical->virtual
105/// column expander for chiplets.
106///
107/// Built once per chiplet, generates
108/// `virtual_layout()`, `parse_row()`,
109/// and `expand_variants()` from the
110/// same packing specification.
111#[derive(Clone, Debug)]
112pub struct VirtualExpander {
113    entries: Vec<CompiledEntry>,
114    num_virtual: usize,
115    num_physical: usize,
116    physical_row_bytes: usize,
117    virtual_layout: Vec<ColumnType>,
118    error: Option<Error>,
119}
120
121impl VirtualExpander {
122    pub fn new() -> Self {
123        Self {
124            entries: Vec::new(),
125            num_virtual: 0,
126            num_physical: 0,
127            physical_row_bytes: 0,
128            virtual_layout: Vec::new(),
129            error: None,
130        }
131    }
132
133    /// Finalize the builder. Returns `Err` if any
134    /// builder step recorded a validation error.
135    pub fn build(self) -> Result<Self, Error> {
136        match self.error {
137            Some(e) => Err(e),
138            None => Ok(self),
139        }
140    }
141
142    /// N physical columns of `storage` type
143    /// to N × bit_width virtual Bit columns.
144    pub fn expand_bits(mut self, count: usize, storage: ColumnType) -> Self {
145        if self.error.is_some() {
146            return self;
147        }
148
149        let bits_per = match expand_bit_width(storage) {
150            Ok(v) => v,
151            Err(e) => {
152                self.error = Some(e);
153                return self;
154            }
155        };
156
157        let byte_offset = self.physical_row_bytes;
158        let phy_col_start = self.num_physical;
159
160        self.entries.push(CompiledEntry {
161            phy_col_start,
162            byte_offset,
163            kind: EntryKind::ExpandBits { count, storage },
164            reuse: false,
165        });
166
167        let virt_count = count * bits_per;
168        self.virtual_layout
169            .extend(repeat_n(ColumnType::Bit, virt_count));
170
171        self.num_virtual += virt_count;
172        self.num_physical += count;
173        self.physical_row_bytes += count * storage.byte_size();
174
175        self
176    }
177
178    /// N physical columns pass through
179    /// 1:1 as virtual columns.
180    pub fn pass_through(mut self, count: usize, storage: ColumnType) -> Self {
181        let byte_offset = self.physical_row_bytes;
182        let phy_col_start = self.num_physical;
183
184        self.entries.push(CompiledEntry {
185            phy_col_start,
186            byte_offset,
187            kind: EntryKind::PassThrough { count, storage },
188            reuse: false,
189        });
190
191        self.virtual_layout.extend(repeat_n(storage, count));
192
193        self.num_virtual += count;
194        self.num_physical += count;
195        self.physical_row_bytes += count * storage.byte_size();
196
197        self
198    }
199
200    /// N physical Bit columns pass through 1:1.
201    pub fn control_bits(mut self, count: usize) -> Self {
202        let byte_offset = self.physical_row_bytes;
203        let phy_col_start = self.num_physical;
204
205        self.entries.push(CompiledEntry {
206            phy_col_start,
207            byte_offset,
208            kind: EntryKind::ControlBits { count },
209            reuse: false,
210        });
211
212        self.virtual_layout.extend(repeat_n(ColumnType::Bit, count));
213
214        self.num_virtual += count;
215        self.num_physical += count;
216        self.physical_row_bytes += count;
217
218        self
219    }
220
221    /// Emit pass-through for columns already
222    /// declared by a prior fresh entry.
223    /// Does not advance the physical cursor.
224    pub fn reuse_pass_through(mut self, phy_col_start: usize, count: usize) -> Self {
225        if self.error.is_some() {
226            return self;
227        }
228
229        if phy_col_start + count > self.num_physical {
230            self.error = Some(Error::Protocol {
231                protocol: "virtual_expand",
232                message: "reuse_pass_through: range exceeds declared physical columns",
233            });
234            return self;
235        }
236
237        let (byte_offset, storage) = match self.find_phy_source(phy_col_start, count) {
238            Ok(v) => v,
239            Err(e) => {
240                self.error = Some(e);
241                return self;
242            }
243        };
244
245        self.entries.push(CompiledEntry {
246            phy_col_start,
247            byte_offset,
248            kind: EntryKind::PassThrough { count, storage },
249            reuse: true,
250        });
251
252        self.virtual_layout.extend(repeat_n(storage, count));
253
254        self.num_virtual += count;
255
256        self
257    }
258
259    /// Emit bit-expansion for columns already
260    /// declared by a prior fresh entry.
261    /// Does not advance the physical cursor.
262    pub fn reuse_expand_bits(mut self, phy_col_start: usize, count: usize) -> Self {
263        if self.error.is_some() {
264            return self;
265        }
266
267        if phy_col_start + count > self.num_physical {
268            self.error = Some(Error::Protocol {
269                protocol: "virtual_expand",
270                message: "reuse_expand_bits: range exceeds declared physical columns",
271            });
272            return self;
273        }
274
275        let (byte_offset, storage) = match self.find_phy_source(phy_col_start, count) {
276            Ok(v) => v,
277            Err(e) => {
278                self.error = Some(e);
279                return self;
280            }
281        };
282
283        let bits_per = match expand_bit_width(storage) {
284            Ok(v) => v,
285            Err(e) => {
286                self.error = Some(e);
287                return self;
288            }
289        };
290
291        self.entries.push(CompiledEntry {
292            phy_col_start,
293            byte_offset,
294            kind: EntryKind::ExpandBits { count, storage },
295            reuse: true,
296        });
297
298        let virt_count = count * bits_per;
299        self.virtual_layout
300            .extend(repeat_n(ColumnType::Bit, virt_count));
301
302        self.num_virtual += virt_count;
303
304        self
305    }
306
307    #[inline]
308    pub fn num_virtual_columns(&self) -> usize {
309        self.num_virtual
310    }
311
312    #[inline]
313    pub fn num_physical_columns(&self) -> usize {
314        self.num_physical
315    }
316
317    #[inline]
318    pub fn physical_row_bytes(&self) -> usize {
319        self.physical_row_bytes
320    }
321
322    #[inline]
323    pub fn virtual_layout(&self) -> &[ColumnType] {
324        &self.virtual_layout
325    }
326
327    /// Verifier-side:
328    /// parse committed physical row bytes
329    /// into virtual field elements.
330    pub fn parse_row<F: TraceCompatibleField>(
331        &self,
332        bytes: &[u8],
333        res: &mut Vec<Flat<F>>,
334    ) -> Result<(), Error> {
335        if bytes.len() != self.physical_row_bytes {
336            return Err(Error::Protocol {
337                protocol: "virtual_expand",
338                message: "parse_row: byte slice length mismatch",
339            });
340        }
341
342        res.reserve(self.num_virtual);
343
344        for entry in &self.entries {
345            let off = entry.byte_offset;
346            match entry.kind {
347                EntryKind::ExpandBits { count, storage } => {
348                    let bsz = storage.byte_size();
349                    let bits = expand_bit_width(storage)?;
350
351                    for i in 0..count {
352                        let start = off + i * bsz;
353                        for bit_idx in 0..bits {
354                            let bit = parse_tower_bit(storage, &bytes[start..start + bsz], bit_idx);
355                            res.push(Flat::from_raw(F::from(Bit::from(bit))));
356                        }
357                    }
358                }
359                EntryKind::PassThrough { count, storage } => {
360                    let bsz = storage.byte_size();
361                    for i in 0..count {
362                        let start = off + i * bsz;
363                        res.push(storage.parse_from_bytes(&bytes[start..start + bsz]));
364                    }
365                }
366                EntryKind::ControlBits { count } => {
367                    for i in 0..count {
368                        res.push(Flat::from_raw(F::from(Bit::from(bytes[off + i] & 1))));
369                    }
370                }
371            }
372        }
373
374        Ok(())
375    }
376
377    /// Prover-side:
378    /// expand physical `ColumnTrace`
379    /// into virtual `PolyVariant`s.
380    pub fn expand_variants<'a, F, T: Trace + ?Sized>(
381        &self,
382        trace: &'a T,
383        phy_start_idx: usize,
384    ) -> Result<Vec<PolyVariant<'a, F>>, Error>
385    where
386        F: TraceCompatibleField + 'static,
387    {
388        let columns = trace.columns();
389
390        let mut variants = Vec::with_capacity(self.num_virtual);
391        for entry in &self.entries {
392            let base = phy_start_idx + entry.phy_col_start;
393            match entry.kind {
394                EntryKind::ExpandBits { count, storage } => {
395                    let bits = expand_bit_width(storage)?;
396                    for i in 0..count {
397                        let col = columns.get(base + i).ok_or(Error::Protocol {
398                            protocol: "virtual_expand",
399                            message: "missing physical column for ExpandBits",
400                        })?;
401
402                        for bit_idx in 0..bits {
403                            variants.push(expand_packed_bit(col, storage, bit_idx)?);
404                        }
405                    }
406                }
407                EntryKind::PassThrough { count, storage } => {
408                    for i in 0..count {
409                        let col = columns.get(base + i).ok_or(Error::Protocol {
410                            protocol: "virtual_expand",
411                            message: "missing physical column for PassThrough",
412                        })?;
413
414                        variants.push(expand_pass_through(col, storage)?);
415                    }
416                }
417                EntryKind::ControlBits { count } => {
418                    for i in 0..count {
419                        let col = columns.get(base + i).ok_or(Error::Protocol {
420                            protocol: "virtual_expand",
421                            message: "missing physical column for ControlBits",
422                        })?;
423                        let data = col.as_bit_slice().ok_or(Error::Protocol {
424                            protocol: "virtual_expand",
425                            message: "control column must be Bit",
426                        })?;
427
428                        variants.push(PolyVariant::BitSlice(data));
429                    }
430                }
431            }
432        }
433
434        Ok(variants)
435    }
436
437    /// Wire-format serialization descriptor.
438    pub fn expansion_entries(&self) -> Vec<ExpansionEntry> {
439        self.entries
440            .iter()
441            .map(|e| match (e.kind, e.reuse) {
442                (EntryKind::PassThrough { count, storage }, true) => {
443                    ExpansionEntry::ReusePassThrough {
444                        phy_col_start: e.phy_col_start,
445                        count,
446                        storage,
447                    }
448                }
449                (EntryKind::ExpandBits { count, storage }, true) => {
450                    ExpansionEntry::ReuseExpandBits {
451                        phy_col_start: e.phy_col_start,
452                        count,
453                        storage,
454                    }
455                }
456                (EntryKind::ExpandBits { count, storage }, false) => {
457                    ExpansionEntry::ExpandBits { count, storage }
458                }
459                (EntryKind::PassThrough { count, storage }, false) => {
460                    ExpansionEntry::PassThrough { count, storage }
461                }
462                (EntryKind::ControlBits { count }, _) => ExpansionEntry::ControlBits { count },
463            })
464            .collect()
465    }
466
467    // Fresh entries have phy_col_start == running_phy;
468    // reuse entries point backward.
469    fn find_phy_source(
470        &self,
471        target_start: usize,
472        target_count: usize,
473    ) -> Result<(usize, ColumnType), Error> {
474        let mut running_phy = 0usize;
475        for entry in &self.entries {
476            if entry.phy_col_start != running_phy {
477                continue;
478            }
479
480            let entry_count = entry.kind.count();
481            let entry_end = running_phy + entry_count;
482
483            if target_start >= running_phy && target_start + target_count <= entry_end {
484                let storage = entry.kind.storage();
485                let offset_in_entry = target_start - running_phy;
486
487                return Ok((
488                    entry.byte_offset + offset_in_entry * storage.byte_size(),
489                    storage,
490                ));
491            }
492
493            running_phy = entry_end;
494        }
495
496        Err(Error::Protocol {
497            protocol: "virtual_expand",
498            message: "reuse: source columns not found in any single fresh entry",
499        })
500    }
501}
502
503impl Default for VirtualExpander {
504    fn default() -> Self {
505        Self::new()
506    }
507}
508
509/// Maps the claimed virtual evals and the committed columns
510/// onto ring-switch binding units. `eta^k` runs once per
511/// unit (in claim order); a bit-expanded physical column
512/// is one Ring unit consuming its `bits` claims.
513pub struct RingSwitchPlan {
514    pub num_units: usize,
515    pub units: Vec<(bool, usize)>,
516    pub phys_rs: Vec<ColumnType>,
517    phys_bit: Vec<Vec<usize>>,
518    phys_whole: Vec<Vec<usize>>,
519}
520
521impl RingSwitchPlan {
522    pub fn new(
523        layout: &[ColumnType],
524        entries: Option<&[ExpansionEntry]>,
525        num_blind: usize,
526    ) -> Result<Self, Error> {
527        let num_phys = layout.len();
528        let total = num_phys + num_blind;
529
530        let mut phys_bit = vec![Vec::new(); total];
531        let mut phys_whole = vec![Vec::new(); total];
532        let mut units: Vec<(bool, usize)> = Vec::new();
533
534        let mut phys_rs: Vec<ColumnType> = layout.iter().map(|ct| ct.rs_field()).collect();
535        phys_rs.extend((0..num_blind).map(|_| ColumnType::B128));
536
537        let bounds = |upper: usize| -> Result<(), Error> {
538            if upper > num_phys {
539                return Err(Error::Protocol {
540                    protocol: "ring_switch_plan",
541                    message: "expansion entry exceeds the physical column layout",
542                });
543            }
544
545            Ok(())
546        };
547
548        match entries {
549            Some(entries) => {
550                let mut running = 0usize;
551                for e in entries {
552                    match *e {
553                        ExpansionEntry::ExpandBits { count, storage } => {
554                            let bits = expand_bit_width(storage)?;
555
556                            bounds(running + count)?;
557
558                            for j in 0..count {
559                                phys_bit[running + j].push(units.len());
560                                units.push((true, bits));
561                            }
562
563                            running += count;
564                        }
565                        ExpansionEntry::PassThrough { count, .. }
566                        | ExpansionEntry::ControlBits { count } => {
567                            bounds(running + count)?;
568
569                            for j in 0..count {
570                                phys_whole[running + j].push(units.len());
571                                units.push((false, 1));
572                            }
573
574                            running += count;
575                        }
576                        ExpansionEntry::ReusePassThrough {
577                            phy_col_start,
578                            count,
579                            ..
580                        } => {
581                            bounds(phy_col_start + count)?;
582
583                            for j in 0..count {
584                                phys_whole[phy_col_start + j].push(units.len());
585                                units.push((false, 1));
586                            }
587                        }
588                        ExpansionEntry::ReuseExpandBits {
589                            phy_col_start,
590                            count,
591                            storage,
592                        } => {
593                            let bits = expand_bit_width(storage)?;
594
595                            bounds(phy_col_start + count)?;
596
597                            for j in 0..count {
598                                phys_bit[phy_col_start + j].push(units.len());
599                                units.push((true, bits));
600                            }
601                        }
602                    }
603                }
604
605                if running != num_phys {
606                    return Err(Error::Protocol {
607                        protocol: "ring_switch_plan",
608                        message: "expansion entries do not cover the physical column layout",
609                    });
610                }
611            }
612            None => {
613                for pw in phys_whole.iter_mut().take(num_phys) {
614                    pw.push(units.len());
615                    units.push((false, 1));
616                }
617            }
618        }
619
620        for b in 0..num_blind {
621            phys_whole[num_phys + b].push(units.len());
622            units.push((false, 1));
623        }
624
625        let num_units = units.len();
626
627        Ok(Self {
628            units,
629            phys_bit,
630            phys_whole,
631            phys_rs,
632            num_units,
633        })
634    }
635
636    pub fn has_ring(&self) -> bool {
637        self.units.iter().any(|(is_ring, _)| *is_ring)
638    }
639
640    pub fn total_claims(&self) -> usize {
641        self.units.iter().map(|(_, n)| n).sum()
642    }
643
644    pub fn opened_row_bytes(&self) -> usize {
645        self.phys_rs.iter().map(|ct| ct.byte_size()).sum()
646    }
647
648    pub fn num_master_vectors(&self) -> usize {
649        1 + usize::from(self.has_ring())
650    }
651
652    pub fn split_vars(&self, num_vars: usize, config: &Config) -> usize {
653        compute_split_vars(
654            num_vars,
655            config.num_queries,
656            config.ldt_support_size,
657            self.opened_row_bytes(),
658            self.num_master_vectors(),
659        )
660    }
661
662    /// Per committed column:
663    /// its base `eta` coefficients in the ring and whole
664    /// masters, plus `eta^U` (the next-row shift multiplier).
665    pub fn column_coeffs<F>(&self, eta: Flat<F>) -> (Vec<Flat<F>>, Vec<Flat<F>>, Flat<F>)
666    where
667        F: HardwareField,
668    {
669        let mut eta_pows = Vec::with_capacity(self.num_units + 1);
670        let mut e = Flat::from_raw(F::ONE);
671
672        for _ in 0..=self.num_units {
673            eta_pows.push(e);
674            e *= eta;
675        }
676
677        let total = self.phys_rs.len();
678
679        let mut coeff_bit = vec![Flat::from_raw(F::ZERO); total];
680        let mut coeff_whole = vec![Flat::from_raw(F::ZERO); total];
681
682        for p in 0..total {
683            for &u in &self.phys_bit[p] {
684                coeff_bit[p] += eta_pows[u];
685            }
686
687            for &u in &self.phys_whole[p] {
688                coeff_whole[p] += eta_pows[u];
689            }
690        }
691
692        (coeff_bit, coeff_whole, eta_pows[self.num_units])
693    }
694}
695
696fn expand_bit_width(storage: ColumnType) -> Result<usize, Error> {
697    match storage {
698        ColumnType::B8 => Ok(8),
699        ColumnType::B16 => Ok(16),
700        ColumnType::B32 => Ok(32),
701        ColumnType::B64 => Ok(64),
702        _ => Err(Error::Protocol {
703            protocol: "virtual_expand",
704            message: "ExpandBits requires B8/B16/B32/B64",
705        }),
706    }
707}
708
709/// Tower-basis bit extraction from LE bytes.
710fn parse_tower_bit(storage: ColumnType, bytes: &[u8], bit_idx: usize) -> u8 {
711    match storage {
712        ColumnType::B8 => Flat::from_raw(Block8(bytes[0])).tower_bit(bit_idx),
713        ColumnType::B16 => {
714            let mut arr = [0u8; 2];
715            arr.copy_from_slice(bytes);
716
717            Flat::from_raw(Block16(u16::from_le_bytes(arr))).tower_bit(bit_idx)
718        }
719        ColumnType::B32 => {
720            let mut arr = [0u8; 4];
721            arr.copy_from_slice(bytes);
722
723            Flat::from_raw(Block32(u32::from_le_bytes(arr))).tower_bit(bit_idx)
724        }
725        ColumnType::B64 => {
726            let mut arr = [0u8; 8];
727            arr.copy_from_slice(bytes);
728
729            Flat::from_raw(Block64(u64::from_le_bytes(arr))).tower_bit(bit_idx)
730        }
731        _ => unreachable!(),
732    }
733}
734
735fn expand_packed_bit<F: TraceCompatibleField + 'static>(
736    col: &'_ TraceColumn,
737    storage: ColumnType,
738    bit_idx: usize,
739) -> Result<PolyVariant<'_, F>, Error> {
740    match storage {
741        ColumnType::B8 => {
742            let data = col.as_b8_slice().ok_or(Error::Protocol {
743                protocol: "virtual_expand",
744                message: "ExpandBits B8: column type mismatch",
745            })?;
746
747            Ok(PolyVariant::PackedBitB8 { data, bit_idx })
748        }
749        ColumnType::B16 => {
750            let data = col.as_b16_slice().ok_or(Error::Protocol {
751                protocol: "virtual_expand",
752                message: "ExpandBits B16: column type mismatch",
753            })?;
754
755            Ok(PolyVariant::PackedBitB16 { data, bit_idx })
756        }
757        ColumnType::B32 => {
758            let data = col.as_b32_slice().ok_or(Error::Protocol {
759                protocol: "virtual_expand",
760                message: "ExpandBits B32: column type mismatch",
761            })?;
762
763            Ok(PolyVariant::PackedBitB32 { data, bit_idx })
764        }
765        ColumnType::B64 => {
766            let data = col.as_b64_slice().ok_or(Error::Protocol {
767                protocol: "virtual_expand",
768                message: "ExpandBits B64: column type mismatch",
769            })?;
770
771            Ok(PolyVariant::PackedBitB64 { data, bit_idx })
772        }
773        _ => unreachable!(),
774    }
775}
776
777fn expand_pass_through<F: TraceCompatibleField + 'static>(
778    col: &TraceColumn,
779    storage: ColumnType,
780) -> Result<PolyVariant<'_, F>, Error> {
781    match storage {
782        ColumnType::Bit => {
783            let data = col.as_bit_slice().ok_or(Error::Protocol {
784                protocol: "virtual_expand",
785                message: "PassThrough Bit: column type mismatch",
786            })?;
787
788            Ok(PolyVariant::BitSlice(data))
789        }
790        ColumnType::B8 => {
791            let data = col.as_b8_slice().ok_or(Error::Protocol {
792                protocol: "virtual_expand",
793                message: "PassThrough B8: column type mismatch",
794            })?;
795
796            Ok(PolyVariant::B8Slice(data))
797        }
798        ColumnType::B16 => {
799            let data = col.as_b16_slice().ok_or(Error::Protocol {
800                protocol: "virtual_expand",
801                message: "PassThrough B16: column type mismatch",
802            })?;
803
804            Ok(PolyVariant::B16Slice(data))
805        }
806        ColumnType::B32 => {
807            let data = col.as_b32_slice().ok_or(Error::Protocol {
808                protocol: "virtual_expand",
809                message: "PassThrough B32: column type mismatch",
810            })?;
811
812            Ok(PolyVariant::B32Slice(data))
813        }
814        ColumnType::B64 => {
815            let data = col.as_b64_slice().ok_or(Error::Protocol {
816                protocol: "virtual_expand",
817                message: "PassThrough B64: column type mismatch",
818            })?;
819
820            Ok(PolyVariant::B64Slice(data))
821        }
822        ColumnType::B128 => {
823            let data = col.as_b128_slice().ok_or(Error::Protocol {
824                protocol: "virtual_expand",
825                message: "PassThrough B128: column type mismatch",
826            })?;
827
828            Ok(PolyVariant::B128Slice(data))
829        }
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use super::*;
836    use hekate_core::trace::TraceBuilder;
837    use hekate_math::{Block128, TowerField};
838
839    fn keccak_expander() -> VirtualExpander {
840        VirtualExpander::new()
841            .expand_bits(25, ColumnType::B64)
842            .expand_bits(1, ColumnType::B64)
843            .reuse_pass_through(0, 25)
844            .control_bits(2)
845            .build()
846            .unwrap()
847    }
848
849    fn keccak_physical_layout() -> Vec<ColumnType> {
850        let mut layout = vec![ColumnType::B64; 26];
851        layout.extend(repeat_n(ColumnType::Bit, 2));
852
853        layout
854    }
855
856    #[test]
857    fn ram_layout() {
858        let e = VirtualExpander::new()
859            .expand_bits(2, ColumnType::B32)
860            .pass_through(13, ColumnType::B32)
861            .pass_through(1, ColumnType::B128)
862            .control_bits(4)
863            .build()
864            .unwrap();
865
866        assert_eq!(e.num_virtual_columns(), 82);
867        assert_eq!(e.num_physical_columns(), 20);
868        assert_eq!(e.physical_row_bytes(), 80);
869
870        let layout = e.virtual_layout();
871
872        assert_eq!(layout.len(), 82);
873        assert!(layout[..64].iter().all(|&t| t == ColumnType::Bit));
874        assert!(layout[64..77].iter().all(|&t| t == ColumnType::B32));
875        assert_eq!(layout[77], ColumnType::B128);
876        assert!(layout[78..82].iter().all(|&t| t == ColumnType::Bit));
877    }
878
879    #[test]
880    fn keccak_layout() {
881        let e = VirtualExpander::new()
882            .expand_bits(25, ColumnType::B64)
883            .expand_bits(1, ColumnType::B64)
884            .reuse_pass_through(0, 25)
885            .control_bits(2)
886            .build()
887            .unwrap();
888
889        assert_eq!(e.num_virtual_columns(), 1691);
890        assert_eq!(e.num_physical_columns(), 28);
891        assert_eq!(e.physical_row_bytes(), 210);
892
893        let layout = e.virtual_layout();
894
895        assert_eq!(layout.len(), 1691);
896        assert!(layout[..1600].iter().all(|&t| t == ColumnType::Bit));
897        assert!(layout[1600..1664].iter().all(|&t| t == ColumnType::Bit));
898        assert!(layout[1664..1689].iter().all(|&t| t == ColumnType::B64));
899        assert!(layout[1689..1691].iter().all(|&t| t == ColumnType::Bit));
900    }
901
902    #[test]
903    fn ring_switch_plan_rejects_uncovered_columns() {
904        let expander = keccak_expander();
905        let entries = expander.expansion_entries();
906
907        let mut layout = keccak_physical_layout();
908
909        assert_eq!(expander.num_physical_columns(), layout.len());
910        assert!(RingSwitchPlan::new(&layout, Some(&entries), 0).is_ok());
911
912        layout.push(ColumnType::B64);
913
914        assert!(RingSwitchPlan::new(&layout, Some(&entries), 0).is_err());
915    }
916
917    #[test]
918    fn ring_switch_plan_folds_every_committed_column() {
919        let entries = keccak_expander().expansion_entries();
920        let layout = keccak_physical_layout();
921
922        let plan = RingSwitchPlan::new(&layout, Some(&entries), 2).unwrap();
923
924        let zero = Flat::from_raw(Block128::ZERO);
925        let eta = Block128(0x2545F4914F6CDD1D_517CC1B727220A95).to_hardware();
926        let (coeff_bit, coeff_whole, _) = plan.column_coeffs::<Block128>(eta);
927
928        for p in 0..plan.phys_rs.len() {
929            assert!(
930                coeff_bit[p] != zero || coeff_whole[p] != zero,
931                "committed column {p} enters no master fold"
932            );
933        }
934    }
935
936    #[test]
937    fn reuse_partial_range() {
938        let e = VirtualExpander::new()
939            .expand_bits(10, ColumnType::B32)
940            .reuse_pass_through(3, 4)
941            .build()
942            .unwrap();
943
944        assert_eq!(e.num_virtual_columns(), 324);
945        assert_eq!(e.num_physical_columns(), 10);
946        assert_eq!(e.physical_row_bytes(), 40);
947
948        let layout = e.virtual_layout();
949
950        assert_eq!(layout[320..324].len(), 4);
951        assert!(layout[320..324].iter().all(|&t| t == ColumnType::B32));
952    }
953
954    #[test]
955    fn reuse_exceeds_declared() {
956        let result = VirtualExpander::new()
957            .expand_bits(5, ColumnType::B32)
958            .reuse_pass_through(3, 5)
959            .build();
960
961        assert!(result.is_err());
962    }
963
964    #[test]
965    fn reuse_expand_bits_from_pass_through() {
966        let e = VirtualExpander::new()
967            .pass_through(4, ColumnType::B64)
968            .reuse_expand_bits(0, 4)
969            .build()
970            .unwrap();
971
972        assert_eq!(e.num_physical_columns(), 4);
973        assert_eq!(e.physical_row_bytes(), 32);
974        assert_eq!(e.num_virtual_columns(), 4 + 256);
975
976        let layout = e.virtual_layout();
977
978        assert!(layout[0..4].iter().all(|&t| t == ColumnType::B64));
979        assert!(layout[4..260].iter().all(|&t| t == ColumnType::Bit));
980    }
981
982    #[test]
983    fn reuse_expand_bits_exceeds_declared() {
984        let result = VirtualExpander::new()
985            .pass_through(4, ColumnType::B64)
986            .reuse_expand_bits(2, 4)
987            .build();
988
989        assert!(result.is_err());
990    }
991
992    #[test]
993    fn reuse_expand_bits_rejects_b128_source() {
994        let result = VirtualExpander::new()
995            .pass_through(1, ColumnType::B128)
996            .reuse_expand_bits(0, 1)
997            .build();
998
999        assert!(result.is_err());
1000    }
1001
1002    #[test]
1003    fn expand_rejects_bit() {
1004        let result = VirtualExpander::new()
1005            .expand_bits(1, ColumnType::Bit)
1006            .build();
1007
1008        assert!(result.is_err());
1009    }
1010
1011    #[test]
1012    fn expand_rejects_b128() {
1013        let result = VirtualExpander::new()
1014            .expand_bits(1, ColumnType::B128)
1015            .build();
1016
1017        assert!(result.is_err());
1018    }
1019
1020    #[test]
1021    fn empty_expander() {
1022        let e = VirtualExpander::new();
1023        assert_eq!(e.num_virtual_columns(), 0);
1024        assert_eq!(e.num_physical_columns(), 0);
1025        assert_eq!(e.physical_row_bytes(), 0);
1026        assert!(e.virtual_layout().is_empty());
1027    }
1028
1029    #[test]
1030    fn parse_row_b32_roundtrip() {
1031        let expander = VirtualExpander::new()
1032            .expand_bits(1, ColumnType::B32)
1033            .pass_through(1, ColumnType::B32)
1034            .control_bits(1)
1035            .build()
1036            .unwrap();
1037
1038        let val: u32 = 0xDEAD_BEEF;
1039        let pass_val: u32 = 0x1234_5678;
1040
1041        let mut bytes = Vec::new();
1042        bytes.extend_from_slice(&val.to_le_bytes());
1043        bytes.extend_from_slice(&pass_val.to_le_bytes());
1044        bytes.push(1);
1045
1046        let mut res: Vec<Flat<Block128>> = Vec::new();
1047        expander.parse_row(&bytes, &mut res).unwrap();
1048
1049        assert_eq!(res.len(), 34);
1050
1051        for (bit_idx, elem) in res.iter().enumerate().take(32) {
1052            let expected = Flat::from_raw(Block32(val)).tower_bit(bit_idx);
1053            let got = elem.tower_bit(0);
1054            assert_eq!(got, expected, "bit {bit_idx} mismatch");
1055        }
1056
1057        let pass = res[32];
1058        assert_eq!(
1059            pass,
1060            <Block128 as hekate_math::FlatPromote<Block32>>::promote_flat(Flat::from_raw(Block32(
1061                pass_val
1062            )))
1063        );
1064
1065        let ctrl = res[33].tower_bit(0);
1066        assert_eq!(ctrl, 1);
1067    }
1068
1069    #[test]
1070    fn expand_variants_b32() {
1071        let expander = VirtualExpander::new()
1072            .expand_bits(1, ColumnType::B32)
1073            .pass_through(1, ColumnType::B32)
1074            .control_bits(1)
1075            .build()
1076            .unwrap();
1077
1078        let layout = [ColumnType::B32, ColumnType::B32, ColumnType::Bit];
1079        let num_vars = 2;
1080
1081        let mut tb = TraceBuilder::new(&layout, num_vars).unwrap();
1082        tb.set_b32(0, 0, Block32(0xAAAA_BBBB)).unwrap();
1083        tb.set_b32(1, 0, Block32(0x1111_2222)).unwrap();
1084        tb.set_bit(2, 0, Bit::ONE).unwrap();
1085
1086        let trace = tb.build();
1087
1088        let variants: Vec<PolyVariant<'_, Block128>> = expander.expand_variants(&trace, 0).unwrap();
1089
1090        assert_eq!(variants.len(), 34);
1091
1092        for (i, v) in variants.iter().enumerate().take(32) {
1093            assert!(matches!(v, PolyVariant::PackedBitB32 { bit_idx, .. } if *bit_idx == i));
1094        }
1095
1096        assert!(matches!(variants[32], PolyVariant::B32Slice(_)));
1097        assert!(matches!(variants[33], PolyVariant::BitSlice(_)));
1098    }
1099}