1use alloc::collections::BTreeMap;
6use alloc::string::String;
7use alloc::vec::Vec;
8use hekate_core::errors;
9use hekate_math::{Flat, HardwareField, TowerField};
10
11use crate::FixedColumn;
12
13mod rank;
14
15pub use rank::{
16 OMEGA, OMEGA_ORDER, RankClock, RankTable, TableHeight, rank_clocks, validate_ordered_buses,
17 validate_ordered_emits,
18};
19
20pub type ChallengeLabel = &'static [u8];
23
24pub const EMIT_RANK_LABEL: ChallengeLabel = b"kappa_emit_rank";
26
27pub const MAX_ROW_INDEX_BYTES: usize = 8;
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub enum BusKind {
38 #[default]
39 Permutation,
40 Lookup,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
46pub enum Side {
47 Request,
48 Response,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum Source {
55 Column(usize),
57
58 Columns(Vec<usize>),
61
62 RowIndexLeBytes(usize),
66
67 Const(u128),
69
70 RowIndexByte(usize),
72
73 PhaseColumn(usize),
77
78 EmitRank(Side),
81}
82
83#[derive(Clone, Copy)]
86pub struct BusWeights<F: HardwareField> {
87 pub consistency: Flat<F>,
88 pub bus_sum: Flat<F>,
89}
90
91#[derive(Clone, Debug)]
104pub struct PermutationCheckSpec {
105 pub kind: BusKind,
106
107 pub sources: Vec<(Source, ChallengeLabel)>,
110
111 pub selector: Option<usize>,
114
115 pub recv_selector: Option<usize>,
118
119 pub clock_waiver: Option<String>,
122}
123
124impl PermutationCheckSpec {
125 pub fn new(sources: Vec<(Source, ChallengeLabel)>, selector: Option<usize>) -> Self {
126 Self {
127 sources,
128 selector,
129 recv_selector: None,
130 kind: BusKind::Permutation,
131 clock_waiver: None,
132 }
133 }
134
135 pub fn new_lookup(sources: Vec<(Source, ChallengeLabel)>, selector: Option<usize>) -> Self {
139 Self {
140 sources,
141 selector,
142 recv_selector: None,
143 kind: BusKind::Lookup,
144 clock_waiver: None,
145 }
146 }
147
148 pub fn new_paired(
152 sources: Vec<(Source, ChallengeLabel)>,
153 s_send: usize,
154 s_recv: usize,
155 kind: BusKind,
156 ) -> Self {
157 Self {
158 sources,
159 selector: Some(s_send),
160 recv_selector: Some(s_recv),
161 kind,
162 clock_waiver: None,
163 }
164 }
165
166 pub fn with_clock_waiver(mut self, reason: impl Into<String>) -> Self {
170 self.clock_waiver = Some(reason.into());
171 self
172 }
173
174 pub fn num_sources(&self) -> usize {
175 self.sources.len()
176 }
177
178 pub fn has_selector(&self) -> bool {
179 self.selector.is_some()
180 }
181
182 pub fn has_paired(&self) -> bool {
183 self.recv_selector.is_some()
184 }
185
186 pub fn shift_column_indices(&mut self, offset: usize) {
189 for (source, _) in &mut self.sources {
190 match source {
191 Source::Column(idx) | Source::PhaseColumn(idx) => *idx += offset,
192 Source::Columns(indices) => {
193 for idx in indices {
194 *idx += offset;
195 }
196 }
197 _ => {}
198 }
199 }
200
201 if let Some(sel_idx) = &mut self.selector {
202 *sel_idx += offset;
203 }
204
205 if let Some(sel_idx) = &mut self.recv_selector {
206 *sel_idx += offset;
207 }
208 }
209
210 pub fn has_real_clock_source(&self) -> bool {
213 self.sources.iter().any(|(src, _)| {
214 matches!(
215 src,
216 Source::RowIndexLeBytes(_) | Source::RowIndexByte(_) | Source::EmitRank(_)
217 )
218 })
219 }
220
221 pub fn validate_clock_stitching(&self, _bus_id: &str) -> errors::Result<()> {
224 for (src, _) in &self.sources {
225 let width_ok = match src {
226 Source::RowIndexLeBytes(n) => *n >= 1 && *n <= MAX_ROW_INDEX_BYTES,
227 Source::RowIndexByte(k) => *k < MAX_ROW_INDEX_BYTES,
228 Source::Column(_)
229 | Source::Columns(_)
230 | Source::Const(_)
231 | Source::PhaseColumn(_)
232 | Source::EmitRank(_) => true,
233 };
234
235 if !width_ok {
236 return Err(errors::Error::Protocol {
237 protocol: "logup_bus",
238 message: "row-index clock wider than 8 bytes; the row-index \
239 MLE folds at most 8 and truncates the rest silently",
240 });
241 }
242 }
243
244 let waiver_status = self.clock_waiver.as_deref().map(WaiverStatus::classify);
245
246 match (self.kind, self.has_real_clock_source(), waiver_status) {
247 (BusKind::Lookup, _, Some(_)) => Err(errors::Error::Protocol {
248 protocol: "logup_bus",
249 message: "lookup bus carries a clock_waiver; waivers only apply \
250 to Permutation kind, drop the .with_clock_waiver(...) call",
251 }),
252 (BusKind::Permutation, true, Some(_)) => Err(errors::Error::Protocol {
253 protocol: "logup_bus",
254 message: "permutation bus carries both a clock source and a \
255 clock_waiver; pick one shape",
256 }),
257 (BusKind::Permutation, false, Some(WaiverStatus::Empty)) => {
258 Err(errors::Error::Protocol {
259 protocol: "logup_bus",
260 message: "permutation bus has an empty clock_waiver; provide a \
261 non-empty reason citing the load-bearing AIR constraint",
262 })
263 }
264 (BusKind::Permutation, false, Some(WaiverStatus::TooShort)) => {
265 Err(errors::Error::Protocol {
266 protocol: "logup_bus",
267 message: "permutation bus has an under-specified clock_waiver; \
268 the reason must be at least 32 chars and cite a file/line \
269 of the load-bearing AIR constraint",
270 })
271 }
272 (BusKind::Permutation, false, Some(WaiverStatus::MissingCitation)) => {
273 Err(errors::Error::Protocol {
274 protocol: "logup_bus",
275 message: "permutation bus clock_waiver lacks a 'see <path>' citation; \
276 waiver text must start with 'see ' followed by the file path \
277 of the load-bearing AIR constraint",
278 })
279 }
280 (BusKind::Permutation, false, None) => Err(errors::Error::Protocol {
281 protocol: "logup_bus",
282 message: "permutation bus lacks per-row clock stitching; give the \
283 Service an EmitRank slot, give a raw spec a RowIndexLeBytes \
284 or RowIndexByte source, or add a clock_waiver citing the AIR \
285 constraint that forces per-row uniqueness",
286 }),
287 _ => Ok(()),
288 }
289 }
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
294pub enum ServiceSlot {
295 Value(ChallengeLabel),
297
298 Const(ChallengeLabel, u128),
301
302 EmitRank,
305}
306
307#[derive(Clone, Debug, PartialEq, Eq)]
310pub struct Service {
311 pub bus_id: &'static str,
312 pub kind: BusKind,
313 pub slots: Vec<ServiceSlot>,
314}
315
316impl Service {
317 pub fn num_values(&self) -> usize {
318 self.slots
319 .iter()
320 .filter(|s| matches!(s, ServiceSlot::Value(_)))
321 .count()
322 }
323
324 pub fn request(
327 &self,
328 values: &[usize],
329 selector: usize,
330 ) -> errors::Result<PermutationCheckSpec> {
331 self.spec(values, Side::Request, selector)
332 }
333
334 pub fn respond(
337 &self,
338 values: &[usize],
339 selector: usize,
340 ) -> errors::Result<PermutationCheckSpec> {
341 self.spec(values, Side::Response, selector)
342 }
343
344 fn spec(
345 &self,
346 values: &[usize],
347 side: Side,
348 selector: usize,
349 ) -> errors::Result<PermutationCheckSpec> {
350 let rank_slots = self
351 .slots
352 .iter()
353 .filter(|s| matches!(s, ServiceSlot::EmitRank))
354 .count();
355
356 let clock_ok = matches!(
357 (self.kind, rank_slots),
358 (BusKind::Permutation, 1) | (BusKind::Lookup, 0)
359 );
360
361 if !clock_ok {
362 return Err(errors::Error::Protocol {
363 protocol: "service",
364 message: "Permutation schema needs exactly one EmitRank slot; \
365 Lookup schema must have none",
366 });
367 }
368
369 if values.len() != self.num_values() {
370 return Err(errors::Error::Protocol {
371 protocol: "service",
372 message: "value column count does not match the schema's Value slots",
373 });
374 }
375
376 let mut sources = Vec::with_capacity(self.slots.len());
377 let mut next_value = values.iter();
378
379 for slot in &self.slots {
380 match slot {
381 ServiceSlot::Value(label) => {
382 let col = next_value.next().ok_or(errors::Error::Protocol {
383 protocol: "service",
384 message: "value column count does not match the schema's Value slots",
385 })?;
386
387 sources.push((Source::Column(*col), *label));
388 }
389 ServiceSlot::Const(label, v) => {
390 sources.push((Source::Const(*v), *label));
391 }
392 ServiceSlot::EmitRank => {
393 sources.push((Source::EmitRank(side), EMIT_RANK_LABEL));
394 }
395 }
396 }
397
398 Ok(match self.kind {
399 BusKind::Permutation => PermutationCheckSpec::new(sources, Some(selector)),
400 BusKind::Lookup => PermutationCheckSpec::new_lookup(sources, Some(selector)),
401 })
402 }
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq)]
406enum WaiverStatus {
407 Empty,
408 TooShort,
409 MissingCitation,
410 Ok,
411}
412
413impl WaiverStatus {
414 const MIN_WAIVER_LEN: usize = 32;
415 const REQUIRED_PREFIX: &'static str = "see ";
416
417 fn classify(s: &str) -> Self {
418 if s.is_empty() {
419 Self::Empty
420 } else if s.len() < Self::MIN_WAIVER_LEN {
421 Self::TooShort
422 } else if !s.starts_with(Self::REQUIRED_PREFIX) {
423 Self::MissingCitation
424 } else {
425 Self::Ok
426 }
427 }
428}
429
430pub fn validate_fixed_selectors<F>(
434 specs: &[(String, PermutationCheckSpec)],
435 fixed: &[FixedColumn<F>],
436) -> errors::Result<()> {
437 for (_, spec) in specs {
438 if spec.selector.is_none() && spec.recv_selector.is_some() {
439 return Err(errors::Error::Protocol {
440 protocol: "logup_bus",
441 message: "paired bus has recv_selector without send selector",
442 });
443 }
444
445 let phases = spec.sources.iter().filter_map(|(src, _)| match src {
446 Source::PhaseColumn(col) => Some(*col),
447 _ => None,
448 });
449
450 for sel in [spec.selector, spec.recv_selector]
451 .into_iter()
452 .flatten()
453 .chain(phases)
454 {
455 let Some(fc) = fixed.iter().find(|fc| fc.col_idx == sel) else {
456 return Err(errors::Error::Protocol {
457 protocol: "logup_bus",
458 message: "bus selector or clock phase reads a witness column; \
459 every selector, recv_selector and PhaseColumn must be \
460 None or a member of the table's fixed_columns",
461 });
462 };
463
464 if !fc.shape.is_overlay() {
465 return Err(errors::Error::Protocol {
466 protocol: "logup_bus",
467 message: "bus selector or clock phase is pinned to a substituted \
468 shape; FirstRow/LastRow/Custom replace the committed \
469 column with a virtual poly the bus cannot read; pin \
470 with Cadence, Segments, Periodic, Sparse or Dense",
471 });
472 }
473 }
474 }
475
476 Ok(())
477}
478
479pub fn validate_bus_set<'a, I>(endpoints: I) -> errors::Result<()>
482where
483 I: IntoIterator<Item = (&'a str, &'a PermutationCheckSpec)>,
484{
485 let mut by_bus: BTreeMap<&'a str, Vec<&'a PermutationCheckSpec>> = BTreeMap::new();
486
487 for (bus_id, spec) in endpoints {
488 if !bus_id.bytes().all(|b| b.is_ascii_graphic()) {
489 return Err(errors::Error::Protocol {
490 protocol: "logup_bus",
491 message: "bus_id has a non-graphic byte; bus ids must be \
492 graphic ASCII (0x21..=0x7E). A space, zero-width, \
493 or homoglyph byte forges a visually identical \
494 second bus that balances on its own",
495 });
496 }
497
498 by_bus.entry(bus_id).or_default().push(spec);
499 }
500
501 for specs in by_bus.values() {
502 let any_lookup = specs.iter().any(|s| s.kind == BusKind::Lookup);
503 let any_perm = specs.iter().any(|s| s.kind == BusKind::Permutation);
504
505 if any_lookup && any_perm {
506 return Err(errors::Error::Protocol {
507 protocol: "logup_bus",
508 message: "bus_id has mixed BusKind across endpoints; \
509 all endpoints must agree on Permutation or Lookup",
510 });
511 }
512 }
513
514 Ok(())
515}
516
517pub fn accumulate_lookup_heights(
522 specs: &[(String, PermutationCheckSpec)],
523 table_rows: u64,
524 heights: &mut BTreeMap<String, u64>,
525) {
526 for (bus_id, spec) in specs {
527 if spec.kind == BusKind::Lookup {
528 let entry = heights.entry(bus_id.clone()).or_insert(0);
529 *entry = (*entry).max(table_rows);
530 }
531 }
532}
533
534pub fn eval_row_idx_le_mle<F>(num_bytes: usize, r_final: &[Flat<F>]) -> Flat<F>
537where
538 F: TowerField + HardwareField + From<u128>,
539{
540 let total_bits = (num_bytes.min(8) * 8).min(r_final.len());
541
542 let mut acc = Flat::from_raw(F::ZERO);
543 for (i, r) in r_final.iter().enumerate().take(total_bits) {
544 acc += F::from(1u128 << i).to_hardware() * *r;
545 }
546
547 acc
548}
549
550pub fn eval_row_idx_byte_mle<F>(byte_idx: usize, r_final: &[Flat<F>]) -> Flat<F>
553where
554 F: TowerField + HardwareField + From<u128>,
555{
556 let bit_start = byte_idx.saturating_mul(8);
557 if bit_start >= r_final.len() {
558 return Flat::from_raw(F::ZERO);
559 }
560
561 let end = (bit_start + 8).min(r_final.len());
562
563 let mut acc = Flat::from_raw(F::ZERO);
564 for (j, i) in (bit_start..end).enumerate() {
565 acc += F::from(1u128 << j).to_hardware() * r_final[i];
566 }
567
568 acc
569}
570
571pub fn bus_alpha_weights<F: HardwareField>(
575 start: Flat<F>,
576 alpha: Flat<F>,
577) -> impl Iterator<Item = BusWeights<F>> {
578 let alpha_sq = alpha * alpha;
579
580 core::iter::successors(Some(start), move |&w| Some(w * alpha_sq)).map(move |w| BusWeights {
581 consistency: w,
582 bus_sum: w * alpha,
583 })
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use crate::FixedShape;
590 use hekate_math::Block128;
591
592 fn test_service() -> Service {
593 Service {
594 bus_id: "svc",
595 kind: BusKind::Permutation,
596 slots: vec![
597 ServiceSlot::Value(b"k_a"),
598 ServiceSlot::Const(b"k_dom", 7),
599 ServiceSlot::EmitRank,
600 ServiceSlot::Value(b"k_dir"),
601 ],
602 }
603 }
604
605 #[test]
606 fn permutation_spec_creation() {
607 let sources = vec![
608 (Source::Column(0), b"kappa_0" as ChallengeLabel),
609 (Source::Column(1), b"kappa_1" as ChallengeLabel),
610 (Source::RowIndexLeBytes(4), b"kappa_clk" as ChallengeLabel),
611 ];
612
613 let spec = PermutationCheckSpec::new(sources, Some(2));
614
615 assert_eq!(spec.num_sources(), 3);
616 assert!(spec.has_selector());
617 assert_eq!(spec.selector, Some(2));
618 }
619
620 #[test]
621 fn source_variants() {
622 let col = Source::Column(5);
623 let cols = Source::Columns(vec![0, 1, 2, 3]);
624 let clock = Source::RowIndexLeBytes(4);
625 let constant = Source::Const(0x01);
626
627 assert_eq!(col, Source::Column(5));
628 assert_eq!(cols, Source::Columns(vec![0, 1, 2, 3]));
629 assert_eq!(clock, Source::RowIndexLeBytes(4));
630 assert_eq!(constant, Source::Const(0x01));
631 }
632
633 #[test]
634 fn new_paired_populates_both_selectors() {
635 let spec = PermutationCheckSpec::new_paired(
636 vec![(Source::Column(0), b"k_a" as ChallengeLabel)],
637 3,
638 5,
639 BusKind::Permutation,
640 );
641
642 assert!(spec.has_selector());
643 assert!(spec.has_paired());
644 assert_eq!(spec.selector, Some(3));
645 assert_eq!(spec.recv_selector, Some(5));
646 assert_eq!(spec.kind, BusKind::Permutation);
647 }
648
649 #[test]
650 fn new_defaults_recv_selector_none() {
651 let spec =
652 PermutationCheckSpec::new(vec![(Source::Column(0), b"k_a" as ChallengeLabel)], Some(1));
653
654 assert!(!spec.has_paired());
655 assert_eq!(spec.recv_selector, None);
656 }
657
658 #[test]
659 fn new_lookup_defaults_recv_selector_none() {
660 let spec = PermutationCheckSpec::new_lookup(
661 vec![(Source::Column(0), b"k_a" as ChallengeLabel)],
662 Some(1),
663 );
664
665 assert!(!spec.has_paired());
666 assert_eq!(spec.recv_selector, None);
667 }
668
669 #[test]
670 fn shift_column_indices_covers_recv_selector() {
671 let mut spec = PermutationCheckSpec::new_paired(
672 vec![
673 (Source::Column(0), b"k_a" as ChallengeLabel),
674 (Source::Columns(vec![1, 2]), b"k_b" as ChallengeLabel),
675 ],
676 3,
677 5,
678 BusKind::Lookup,
679 );
680
681 spec.shift_column_indices(10);
682
683 assert_eq!(spec.selector, Some(13));
684 assert_eq!(spec.recv_selector, Some(15));
685
686 match &spec.sources[0].0 {
687 Source::Column(idx) => assert_eq!(*idx, 10),
688 other => panic!("expected Column, got {other:?}"),
689 }
690
691 match &spec.sources[1].0 {
692 Source::Columns(idxs) => assert_eq!(idxs, &vec![11, 12]),
693 other => panic!("expected Columns, got {other:?}"),
694 }
695 }
696
697 #[test]
698 fn validate_bus_set_rejects_homoglyph_split_bus() {
699 let clockless =
700 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1));
701 let other = clockless.clone();
702
703 assert!(
706 validate_bus_set(vec![("ram_link", &clockless), ("ram_link\u{200b}", &other)]).is_err()
707 );
708 }
709
710 #[test]
711 fn validate_bus_set_accepts_ascii_bus_id() {
712 let spec =
713 PermutationCheckSpec::new(vec![(Source::Column(0), b"k" as ChallengeLabel)], Some(1));
714
715 assert!(validate_bus_set(vec![("ram_link", &spec)]).is_ok());
716 }
717
718 #[test]
719 fn service_endpoints_share_schema() {
720 let svc = test_service();
721
722 let req = svc.request(&[10, 11], 12).unwrap();
723 let resp = svc.respond(&[20, 21], 23).unwrap();
724
725 let req_labels: Vec<_> = req.sources.iter().map(|(_, l)| *l).collect();
726 let resp_labels: Vec<_> = resp.sources.iter().map(|(_, l)| *l).collect();
727
728 assert_eq!(req_labels, resp_labels);
729 assert_eq!(req_labels[2], EMIT_RANK_LABEL);
730
731 assert_eq!(req.sources[0].0, Source::Column(10));
732 assert_eq!(req.sources[1].0, Source::Const(7));
733 assert_eq!(req.sources[2].0, Source::EmitRank(Side::Request));
734 assert_eq!(req.sources[3].0, Source::Column(11));
735 assert_eq!(req.selector, Some(12));
736
737 assert_eq!(resp.sources[0].0, Source::Column(20));
738 assert_eq!(resp.sources[1].0, Source::Const(7));
739 assert_eq!(resp.sources[2].0, Source::EmitRank(Side::Response));
740 assert_eq!(resp.sources[3].0, Source::Column(21));
741 assert_eq!(resp.selector, Some(23));
742
743 req.validate_clock_stitching("svc").unwrap();
744 resp.validate_clock_stitching("svc").unwrap();
745
746 validate_bus_set(vec![("svc", &req), ("svc", &resp)]).unwrap();
747 }
748
749 #[test]
750 fn service_rejects_schema_mismatches() {
751 let svc = test_service();
752
753 assert!(svc.request(&[10], 12).is_err());
754 assert!(svc.respond(&[20, 21, 22], 23).is_err());
755
756 let lookup = Service {
757 bus_id: "rom",
758 kind: BusKind::Lookup,
759 slots: vec![ServiceSlot::Value(b"k_a"), ServiceSlot::EmitRank],
760 };
761
762 assert!(lookup.respond(&[3], 4).is_err());
763
764 let clockless = Service {
765 bus_id: "bad",
766 kind: BusKind::Permutation,
767 slots: vec![ServiceSlot::Value(b"k_a")],
768 };
769
770 assert!(clockless.request(&[1], 2).is_err());
771
772 let two_ranks = Service {
773 bus_id: "bad",
774 kind: BusKind::Permutation,
775 slots: vec![
776 ServiceSlot::EmitRank,
777 ServiceSlot::Value(b"k_a"),
778 ServiceSlot::EmitRank,
779 ],
780 };
781
782 assert!(two_ranks.request(&[1], 2).is_err());
783 }
784
785 #[test]
786 fn lookup_service_derives_lookup_specs() {
787 let svc = Service {
788 bus_id: "rom",
789 kind: BusKind::Lookup,
790 slots: vec![ServiceSlot::Value(b"k_a")],
791 };
792
793 let req = svc.request(&[1], 2).unwrap();
794 let resp = svc.respond(&[3], 4).unwrap();
795
796 assert_eq!(req.kind, BusKind::Lookup);
797 assert_eq!(resp.kind, BusKind::Lookup);
798 assert_eq!(req.sources.len(), 1);
799 }
800
801 #[test]
802 fn column_clock_needs_waiver_whatever_its_label() {
803 let spoofed = PermutationCheckSpec::new(
804 vec![
805 (Source::Column(0), b"k_a" as ChallengeLabel),
806 (Source::Column(1), EMIT_RANK_LABEL),
807 ],
808 Some(2),
809 );
810
811 assert!(spoofed.validate_clock_stitching("svc").is_err());
812
813 spoofed
814 .with_clock_waiver(
815 "see permutation/mod.rs: a body constraint forces per-row uniqueness",
816 )
817 .validate_clock_stitching("svc")
818 .unwrap();
819 }
820
821 #[test]
822 fn witness_clock_phase_is_rejected() {
823 let phased = PermutationCheckSpec::new(
824 vec![
825 (Source::Column(10), b"k_a" as ChallengeLabel),
826 (Source::RowIndexByte(0), b"k_clk_b0" as ChallengeLabel),
827 (Source::PhaseColumn(60), b"k_clk_b1" as ChallengeLabel),
828 ],
829 Some(12),
830 );
831 let specs = vec![(String::from("svc"), phased)];
832
833 let selector_only = vec![FixedColumn::<Block128> {
834 col_idx: 12,
835 shape: FixedShape::Periodic {
836 period: 1,
837 values: vec![Block128::ONE],
838 },
839 }];
840
841 assert!(validate_fixed_selectors(&specs, &selector_only).is_err());
842
843 let mut pinned = selector_only;
844 pinned.push(FixedColumn {
845 col_idx: 60,
846 shape: FixedShape::Periodic {
847 period: 2,
848 values: vec![Block128::ZERO, Block128::ONE],
849 },
850 });
851
852 validate_fixed_selectors(&specs, &pinned).unwrap();
853
854 let mut substituted = pinned;
855 substituted[1].shape = FixedShape::LastRow;
856
857 assert!(validate_fixed_selectors(&specs, &substituted).is_err());
858 }
859
860 #[test]
861 fn bus_alpha_weights_take_consecutive_powers() {
862 let alpha = Block128::from(0x9E37_79B9u128).to_hardware();
863 let start = Block128::from(0x1234u128).to_hardware();
864
865 let mut expected = start;
866 for w in bus_alpha_weights(start, alpha).take(4) {
867 assert_eq!(w.consistency, expected);
868 expected *= alpha;
869
870 assert_eq!(w.bus_sum, expected);
871 expected *= alpha;
872 }
873 }
874}