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