pub const NODES: usize = 8;
const fn presentation_eq(left: PrimePresentation, right: PrimePresentation) -> bool {
matches!(
(left, right),
(PrimePresentation::Residue, PrimePresentation::Residue)
| (
PrimePresentation::MersenneFold,
PrimePresentation::MersenneFold
)
| (
PrimePresentation::GoldilocksFold,
PrimePresentation::GoldilocksFold
)
)
}
const fn storage_eq(left: Storage, right: Storage) -> bool {
match (left, right) {
(Storage::Components, Storage::Components) => true,
(Storage::Packed { width: a }, Storage::Packed { width: b })
| (Storage::PackedPadded { width: a }, Storage::PackedPadded { width: b }) => a == b,
_ => false,
}
}
pub const COORDINATES: usize = 64;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PrimePresentation {
Residue,
MersenneFold,
GoldilocksFold,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Storage {
Components,
Packed {
width: u8,
},
PackedPadded {
width: u8,
},
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Copy)]
pub enum Node {
Unused,
Prime {
modulus: u64,
presentation: PrimePresentation,
},
Polynomial {
degree: u8,
full: u128,
},
Basis {
base: u8,
to_base: [u8; 8],
from_base: [u8; 8],
},
Extension {
base: u8,
relative_degree: usize,
characteristic: u64,
base_degree: u32,
base_words: usize,
coefficients: [u64; COORDINATES],
storage: Storage,
},
}
#[derive(Clone, Copy)]
pub struct Descriptor {
nodes: [Node; NODES],
len: u8,
root: u8,
}
const fn check_storage(storage: Storage, characteristic: u64, degree: u32) {
match storage {
Storage::Components => {}
Storage::Packed { width } => {
assert!(
characteristic == 2,
"packed storage requires characteristic two"
);
assert!(
matches!(width, 8 | 16 | 32 | 64),
"packed storage word width is unsupported"
);
assert!(
width as u32 == degree,
"packed storage width differs from the field degree"
);
}
Storage::PackedPadded { width } => {
assert!(
characteristic == 2,
"packed storage requires characteristic two"
);
assert!(
matches!(width, 8 | 16 | 32 | 64),
"packed storage word width is unsupported"
);
assert!(
degree < width as u32,
"padded packed storage is not narrower than its word"
);
}
}
}
impl Descriptor {
#[must_use]
pub const fn prime(modulus: u64, presentation: PrimePresentation) -> Self {
let mut nodes = [Node::Unused; NODES];
nodes[0] = Node::Prime {
modulus,
presentation,
};
Self {
nodes,
len: 1,
root: 0,
}
}
#[must_use]
pub const fn polynomial(degree: u8, full: u128) -> Self {
let mut nodes = [Node::Unused; NODES];
nodes[0] = Node::Polynomial { degree, full };
Self {
nodes,
len: 1,
root: 0,
}
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub const fn append_basis(&self, to_base: [u8; 8], from_base: [u8; 8]) -> Self {
assert!(self.len < NODES as u8, "description capacity exhausted");
let mut next = *self;
next.nodes[self.len as usize] = Node::Basis {
base: self.root,
to_base,
from_base,
};
next.root = next.len;
next.len += 1;
next
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub const fn append_quadratic_words(&self, linear: u64, constant: u64) -> Self {
assert!(self.len < NODES as u8, "description capacity exhausted");
let half = super::arithmetic::degree_of(self, self.root);
let width = 2 * half;
let storage = if width < 8 {
Storage::PackedPadded { width: 8 }
} else {
Storage::Packed { width: width as u8 }
};
let mut coefficients = [0u64; COORDINATES];
coefficients[0] = constant;
coefficients[1] = linear;
let mut next = *self;
next.nodes[self.len as usize] = Node::Extension {
base: self.root,
relative_degree: 2,
characteristic: 2,
base_degree: half,
base_words: super::arithmetic::element_words(self, self.root),
coefficients,
storage,
};
next.root = next.len;
next.len += 1;
next
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub const fn extension(
base: &Self,
characteristic: u64,
degree: u32,
relative_degree: usize,
relation: &[u64],
storage: Storage,
) -> Self {
assert!(
crate::field::is_prime(characteristic),
"characteristic is not prime"
);
crate::field::finite_field::metadata_order(characteristic, degree);
assert!(
relation.len() <= COORDINATES,
"relation exceeds the coordinate capacity"
);
assert!(
super::arithmetic::characteristic_of(base, base.root) == characteristic,
"extension base characteristic differs from the field characteristic"
);
assert!(
relative_degree >= 2,
"extension relative degree is below two"
);
let base_degree = super::arithmetic::degree_of(base, base.root);
let Some(product) = (base_degree as u64).checked_mul(relative_degree as u64) else {
panic!("extension degree is not the base degree times the relative degree");
};
assert!(
product == degree as u64,
"extension degree is not the base degree times the relative degree"
);
assert!(
relation.len() == degree as usize,
"relation coordinate count differs from the field degree"
);
let mut i = 0;
while i < relation.len() {
assert!(
relation[i] < characteristic,
"relation coordinate is not below the characteristic"
);
i += 1;
}
check_storage(storage, characteristic, degree);
assert!(base.len < NODES as u8, "description capacity exhausted");
let base_words = super::arithmetic::element_words(base, base.root);
let mut coefficients = [0u64; COORDINATES];
if characteristic == 2 {
let mut j = 0;
while j < relative_degree {
let mut word = 0u64;
let mut k = 0;
while k < base_degree as usize {
word |= (relation[j * base_degree as usize + k] & 1) << k;
k += 1;
}
coefficients[j] = word;
j += 1;
}
} else {
let mut i = 0;
while i < relation.len() {
coefficients[i] = relation[i];
i += 1;
}
}
let mut next = *base;
next.nodes[base.len as usize] = Node::Extension {
base: base.root,
relative_degree,
characteristic,
base_degree,
base_words,
coefficients,
storage,
};
next.root = base.len;
next.len = base.len + 1;
next
}
#[must_use]
pub const fn node(&self, index: u8) -> &Node {
&self.nodes[index as usize]
}
#[must_use]
pub const fn root_index(&self) -> u8 {
self.root
}
#[must_use]
pub const fn node_count(&self) -> u8 {
self.len
}
#[must_use]
#[allow(dead_code)]
pub const fn characteristic(&self) -> u64 {
super::arithmetic::characteristic_of(self, self.root)
}
#[must_use]
pub const fn degree(&self) -> u32 {
super::arithmetic::degree_of(self, self.root)
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub const fn base_descriptor(&self) -> Self {
let base = match self.node(self.root) {
Node::Basis { base, .. } | Node::Extension { base, .. } => *base,
_ => panic!("the root node has no base"),
};
let mut sub = *self;
sub.root = base;
sub.len = base + 1;
let mut index = sub.len;
while index < NODES as u8 {
sub.nodes[index as usize] = Node::Unused;
index += 1;
}
sub
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub const fn same_structure(&self, other: &Self) -> bool {
if self.len != other.len || self.root != other.root {
return false;
}
let mut i = 0;
while i < self.len as usize {
let same = match (self.node(i as u8), other.node(i as u8)) {
(
Node::Prime {
modulus: m0,
presentation: p0,
},
Node::Prime {
modulus: m1,
presentation: p1,
},
) => *m0 == *m1 && presentation_eq(*p0, *p1),
(
Node::Polynomial {
degree: d0,
full: f0,
},
Node::Polynomial {
degree: d1,
full: f1,
},
) => *d0 == *d1 && *f0 == *f1,
(
Node::Basis {
base: b0,
to_base: t0,
from_base: i0,
},
Node::Basis {
base: b1,
to_base: t1,
from_base: i1,
},
) => {
let mut eq = *b0 == *b1;
let mut k = 0;
while k < 8 {
eq = eq && t0[k] == t1[k] && i0[k] == i1[k];
k += 1;
}
eq
}
(
Node::Extension {
base: b0,
relative_degree: d0,
coefficients: c0,
storage: s0,
..
},
Node::Extension {
base: b1,
relative_degree: d1,
coefficients: c1,
storage: s1,
..
},
) => {
let mut eq = *b0 == *b1 && *d0 == *d1 && storage_eq(*s0, *s1);
let mut k = 0;
while k < COORDINATES {
eq = eq && c0[k] == c1[k];
k += 1;
}
eq
}
_ => false,
};
if !same {
return false;
}
i += 1;
}
true
}
#[must_use]
#[allow(dead_code)]
pub const fn is_irreducible(&self) -> bool {
super::irreducibility::root_relation_irreducible(self)
}
pub const fn validate_relation(&self) {
super::irreducibility::validate_relation(self);
}
#[cfg(test)]
#[must_use]
#[allow(clippy::large_types_passed_by_value)]
pub const fn from_parts(nodes: [Node; NODES], len: u8, root: u8) -> Self {
Self { nodes, len, root }
}
#[cfg(test)]
#[must_use]
#[allow(clippy::large_types_passed_by_value)]
pub const fn with_node(&self, index: u8, node: Node) -> Self {
let mut next = *self;
next.nodes[index as usize] = node;
next
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::field::binary::description::BinaryDescription;
const RESIDUE_SEVEN: Descriptor = Descriptor::prime(7, PrimePresentation::Residue);
const RESIDUE_TWO: Descriptor = Descriptor::prime(2, PrimePresentation::Residue);
fn build(
base: &Descriptor,
characteristic: u64,
degree: u32,
relative_degree: usize,
relation: &[u64],
storage: Storage,
) -> Descriptor {
Descriptor::extension(
base,
characteristic,
degree,
relative_degree,
relation,
storage,
)
}
#[test]
#[should_panic(expected = "extension base characteristic differs")]
fn extension_rejects_a_characteristic_mismatch() {
build(&RESIDUE_SEVEN, 5, 2, 2, &[1, 0], Storage::Components);
}
#[test]
#[should_panic(expected = "extension relative degree is below two")]
fn extension_rejects_a_linear_relation() {
build(&RESIDUE_SEVEN, 7, 7, 1, &[0], Storage::Components);
}
#[test]
#[should_panic(expected = "extension degree is not the base degree")]
fn extension_rejects_a_wrong_degree() {
build(&RESIDUE_SEVEN, 7, 8, 2, &[0; 8], Storage::Components);
}
#[test]
#[should_panic(expected = "binary field degree exceeds 64")]
fn extension_rejects_a_binary_degree_above_sixty_four() {
let base = <crate::field::binary::tower::FanPaar64Field as crate::field::private::RowAccess>::DESCRIPTOR;
let wide = build(base, 2, 128, 2, &[], Storage::Packed { width: 64 });
let _ = wide;
}
#[test]
#[should_panic(expected = "field degree exceeds 64")]
fn extension_rejects_an_odd_degree_above_sixty_four() {
let wide = build(&RESIDUE_SEVEN, 7, 65, 65, &[0; 65], Storage::Components);
let _ = wide;
}
#[test]
#[should_panic(expected = "binary field degree exceeds 64")]
fn extension_rejects_a_binary_degree_of_sixty_five() {
let wide = build(&RESIDUE_TWO, 2, 65, 65, &[0; 65], Storage::Components);
let _ = wide;
}
#[test]
#[should_panic(expected = "field order overflows u128")]
fn extension_rejects_a_cardinality_overflow() {
build(&RESIDUE_SEVEN, 7, 64, 64, &[], Storage::Components);
}
#[test]
#[should_panic(expected = "relation coordinate count differs")]
fn extension_rejects_a_wrong_coordinate_count() {
build(&RESIDUE_SEVEN, 7, 2, 2, &[0], Storage::Components);
}
#[test]
#[should_panic(expected = "relation coordinate is not below")]
fn extension_rejects_an_out_of_range_coordinate() {
build(&RESIDUE_SEVEN, 7, 2, 2, &[7, 0], Storage::Components);
}
#[test]
#[should_panic(expected = "packed storage requires characteristic two")]
fn extension_rejects_packed_storage_in_odd_characteristic() {
build(
&RESIDUE_SEVEN,
7,
2,
2,
&[0, 0],
Storage::Packed { width: 16 },
);
}
#[test]
#[should_panic(expected = "packed storage word width is unsupported")]
fn extension_rejects_an_unsupported_word_width() {
build(
&RESIDUE_TWO,
2,
2,
2,
&[1, 1],
Storage::PackedPadded { width: 24 },
);
}
#[test]
#[should_panic(expected = "packed storage width differs from the field degree")]
fn extension_rejects_a_packed_width_above_the_degree() {
build(&RESIDUE_TWO, 2, 2, 2, &[1, 1], Storage::Packed { width: 8 });
}
#[test]
#[should_panic(expected = "padded packed storage is not narrower")]
fn extension_rejects_a_padded_width_at_the_degree() {
let aes_row = BinaryDescription::polynomial(8, 0x11B);
let aes = aes_row.as_descriptor();
build(aes, 2, 16, 2, &[0; 16], Storage::PackedPadded { width: 16 });
}
#[test]
#[should_panic(expected = "description capacity exhausted")]
fn extension_rejects_a_full_node_array() {
let identity = [1u8, 2, 4, 8, 0, 0, 0, 0];
let mut base = BinaryDescription::polynomial(8, 0x11B);
for _ in 0..7 {
base = BinaryDescription::append_basis(&base, identity, identity);
}
build(
base.as_descriptor(),
2,
16,
2,
&[0; 16],
Storage::Packed { width: 16 },
);
}
#[test]
fn structural_equality_distinguishes_presentations_and_data() {
let residue = RESIDUE_SEVEN;
let fold = Descriptor::prime(7, PrimePresentation::MersenneFold);
assert!(residue.same_structure(&residue));
assert!(!residue.same_structure(&fold));
let components = build(&RESIDUE_TWO, 2, 2, 2, &[1, 1], Storage::Components);
let padded = build(
&RESIDUE_TWO,
2,
2,
2,
&[1, 1],
Storage::PackedPadded { width: 8 },
);
assert!(!components.same_structure(&padded));
let other = build(&RESIDUE_TWO, 2, 2, 2, &[1, 0], Storage::Components);
assert!(!components.same_structure(&other));
assert!(components.same_structure(&build(
&RESIDUE_TWO,
2,
2,
2,
&[1, 1],
Storage::Components
)));
}
#[test]
fn accessors_read_the_root() {
let quadratic = build(&RESIDUE_SEVEN, 7, 2, 2, &[3, 0], Storage::Components);
assert_eq!(quadratic.characteristic(), 7);
assert_eq!(quadratic.degree(), 2);
let base = quadratic.base_descriptor();
assert_eq!(base.degree(), 1);
assert_eq!(base.characteristic(), 7);
assert!(base.same_structure(&RESIDUE_SEVEN));
}
}