use crate::errors::AlkahestError;
use crate::group::GroupError;
use crate::matrix::normal_form::NormalFormError;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FpGroupError {
InvalidGenerator {
letter: i32,
rank: usize,
},
InvalidPresentation {
detail: String,
},
WordSyntax {
position: usize,
detail: String,
},
EnumerationIncomplete {
live_cosets: usize,
cosets_defined: usize,
max_cosets: usize,
},
ProvablyInfinite {
abelian_invariants: String,
},
CosetCapTooLarge {
requested: usize,
max: usize,
},
CosetOutOfRange {
coset: usize,
index: usize,
},
UnsupportedCohomologyDegree {
degree: usize,
max: usize,
},
CohomologyTooLarge {
detail: String,
},
ModuleShape {
detail: String,
},
ActionNotWellDefined {
detail: String,
},
Permutation(GroupError),
NormalForm(NormalFormError),
Internal {
detail: String,
},
WordTooLong {
letters: u128,
max: usize,
},
MultiplicationTableTooLarge {
order: usize,
max: usize,
},
}
impl fmt::Display for FpGroupError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FpGroupError::InvalidGenerator { letter, rank } => write!(
f,
"word letter {letter} names no generator of a presentation with {rank} of them; \
letters are signed and 1-based (+1 is the first generator, -1 its inverse) and \
0 is not a letter"
),
FpGroupError::InvalidPresentation { detail } => {
write!(f, "malformed presentation: {detail}")
}
FpGroupError::WordSyntax { position, detail } => {
write!(f, "cannot parse word at byte {position}: {detail}")
}
FpGroupError::EnumerationIncomplete {
live_cosets,
cosets_defined,
max_cosets,
} => write!(
f,
"coset enumeration did not complete within {max_cosets} cosets ({live_cosets} \
live of {cosets_defined} defined); this is NOT a claim that the group is \
infinite, nor that it is finite"
),
FpGroupError::ProvablyInfinite { abelian_invariants } => write!(
f,
"the group is infinite: its abelianisation is {abelian_invariants}, which has an \
infinite cyclic factor, so no finite order exists to report"
),
FpGroupError::CosetCapTooLarge { requested, max } => write!(
f,
"a coset cap of {requested} is above the ceiling of {max} for this rank; the \
table costs 2 * rank machine words per coset"
),
FpGroupError::CosetOutOfRange { coset, index } => write!(
f,
"coset {coset} is outside 0..{index}; cosets are 0-based and coset 0 is the \
subgroup H itself"
),
FpGroupError::UnsupportedCohomologyDegree { degree, max } => write!(
f,
"group cohomology is implemented for degrees 0..={max}; H^{degree} is not \
available"
),
FpGroupError::CohomologyTooLarge { detail } => {
write!(f, "cohomology computation too large: {detail}")
}
FpGroupError::ModuleShape { detail } => write!(f, "malformed module: {detail}"),
FpGroupError::ActionNotWellDefined { detail } => write!(
f,
"the supplied matrices do not define an action of this group on this module: \
{detail}"
),
FpGroupError::Permutation(e) => write!(f, "{e}"),
FpGroupError::NormalForm(e) => write!(f, "{e}"),
FpGroupError::Internal { detail } => {
write!(f, "internal fp-group invariant violated: {detail}")
}
FpGroupError::WordTooLong { letters, max } => write!(
f,
"this word would have {letters} letters, above the cap {max}; exponents nest, so \
the ceiling is on the length of the result and not on any one exponent"
),
FpGroupError::MultiplicationTableTooLarge { order, max } => write!(
f,
"|G| = {order} is above the multiplication-table cap {max}; the table is {order}^2 \
words and {order}^2 transversal traces"
),
}
}
}
impl std::error::Error for FpGroupError {}
impl From<GroupError> for FpGroupError {
fn from(e: GroupError) -> Self {
FpGroupError::Permutation(e)
}
}
impl From<NormalFormError> for FpGroupError {
fn from(e: NormalFormError) -> Self {
FpGroupError::NormalForm(e)
}
}
impl AlkahestError for FpGroupError {
fn code(&self) -> &'static str {
match self {
FpGroupError::InvalidGenerator { .. } => "E-FPGRP-001",
FpGroupError::InvalidPresentation { .. } => "E-FPGRP-002",
FpGroupError::WordSyntax { .. } => "E-FPGRP-003",
FpGroupError::EnumerationIncomplete { .. } => "E-FPGRP-004",
FpGroupError::ProvablyInfinite { .. } => "E-FPGRP-005",
FpGroupError::CosetCapTooLarge { .. } => "E-FPGRP-006",
FpGroupError::CosetOutOfRange { .. } => "E-FPGRP-007",
FpGroupError::UnsupportedCohomologyDegree { .. } => "E-FPGRP-008",
FpGroupError::CohomologyTooLarge { .. } => "E-FPGRP-009",
FpGroupError::ModuleShape { .. } => "E-FPGRP-010",
FpGroupError::ActionNotWellDefined { .. } => "E-FPGRP-011",
FpGroupError::Internal { .. } => "E-FPGRP-012",
FpGroupError::WordTooLong { .. } => "E-FPGRP-013",
FpGroupError::MultiplicationTableTooLarge { .. } => "E-FPGRP-014",
FpGroupError::Permutation(e) => e.code(),
FpGroupError::NormalForm(e) => e.code(),
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
FpGroupError::InvalidGenerator { .. } => Some(
"build words with `FreeGroup::word`, `FreeGroup::parse` or `Word::generator`, \
whose letters are checked against the rank",
),
FpGroupError::InvalidPresentation { .. } => Some(
"generator names must be non-empty and pairwise distinct, and the rank must be \
at most MAX_FREE_RANK",
),
FpGroupError::WordSyntax { .. } => Some(
"the syntax is a product of powers of generator names and parenthesised \
sub-words, e.g. `a^2`, `(a*b)^5`, `a*b^-1*a^-1*b`; `1` is the identity",
),
FpGroupError::EnumerationIncomplete { .. } => Some(
"raise `max_cosets` if the group may simply be large, or use \
`abelian_invariants()` — which always terminates — to look for an infinite \
cyclic factor; do NOT read this as evidence either way about finiteness",
),
FpGroupError::ProvablyInfinite { .. } => Some(
"ask for `abelian_invariants()` rather than `order()`, or pass to a \
finite-index subgroup or a finite quotient",
),
FpGroupError::CosetCapTooLarge { .. } => Some(
"lower the cap, or reduce the number of generators — the table is 2 * rank \
words wide per coset",
),
FpGroupError::CosetOutOfRange { .. } => {
Some("cosets are 0-based: the valid range is 0..index()")
}
FpGroupError::UnsupportedCohomologyDegree { .. } => Some(
"only H^0, H^1 and H^2 are implemented; H^2 is the one that classifies central \
extensions, and higher degrees need a resolution this module does not build",
),
FpGroupError::CohomologyTooLarge { .. } => Some(
"use a smaller group or a smaller module: the degree-(d+1) cochain group has \
rank(M) * |G|^(d+1) generators, so the cost is cubic in |G| for H^2",
),
FpGroupError::ModuleShape { .. } => Some(
"invariants are non-negative (0 means a Z summand, d >= 1 means Z/d), and there \
must be one square rank-by-rank action matrix per group generator",
),
FpGroupError::ActionNotWellDefined { .. } => Some(
"check that every relator's matrix product is the identity on M and that each \
generator's matrix maps the relation lattice into itself; for a trivial action \
use `GModule::trivial`, which cannot fail either check",
),
FpGroupError::Internal { .. } => {
Some("this is a bug: please report it with the presentation that produced it")
}
FpGroupError::WordTooLong { .. } => Some(
"exponents nest, so the cap is on the length of the resulting word and not on \
any one exponent; reduce the exponent or the length of the base word",
),
FpGroupError::MultiplicationTableTooLarge { .. } => Some(
"the table is |G|^2 words and |G|^2 transversal traces; use \
`permutation_group()`, whose degree is |G| rather than |G|^2, or take a \
smaller quotient",
),
FpGroupError::Permutation(e) => e.remediation(),
FpGroupError::NormalForm(e) => e.remediation(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_own_variant_has_a_distinct_code_in_the_expected_range() {
let all = [
FpGroupError::InvalidGenerator { letter: 0, rank: 2 },
FpGroupError::InvalidPresentation {
detail: String::new(),
},
FpGroupError::WordSyntax {
position: 0,
detail: String::new(),
},
FpGroupError::EnumerationIncomplete {
live_cosets: 1,
cosets_defined: 2,
max_cosets: 3,
},
FpGroupError::ProvablyInfinite {
abelian_invariants: "Z".into(),
},
FpGroupError::CosetCapTooLarge {
requested: 2,
max: 1,
},
FpGroupError::CosetOutOfRange { coset: 5, index: 3 },
FpGroupError::UnsupportedCohomologyDegree { degree: 3, max: 2 },
FpGroupError::CohomologyTooLarge {
detail: String::new(),
},
FpGroupError::ModuleShape {
detail: String::new(),
},
FpGroupError::ActionNotWellDefined {
detail: String::new(),
},
FpGroupError::Internal {
detail: String::new(),
},
FpGroupError::WordTooLong { letters: 1, max: 0 },
FpGroupError::MultiplicationTableTooLarge { order: 2, max: 1 },
];
let codes: Vec<&str> = all.iter().map(|e| e.code()).collect();
assert_eq!(
codes,
[
"E-FPGRP-001",
"E-FPGRP-002",
"E-FPGRP-003",
"E-FPGRP-004",
"E-FPGRP-005",
"E-FPGRP-006",
"E-FPGRP-007",
"E-FPGRP-008",
"E-FPGRP-009",
"E-FPGRP-010",
"E-FPGRP-011",
"E-FPGRP-012",
"E-FPGRP-013",
"E-FPGRP-014",
]
);
for e in &all {
assert!(e.remediation().is_some(), "{e} has no remediation");
}
}
#[test]
fn incomplete_enumeration_is_not_a_claim_of_infiniteness() {
let incomplete = FpGroupError::EnumerationIncomplete {
live_cosets: 1000,
cosets_defined: 1000,
max_cosets: 1000,
};
let infinite = FpGroupError::ProvablyInfinite {
abelian_invariants: "Z^2".into(),
};
assert_ne!(incomplete.code(), infinite.code());
assert!(incomplete.to_string().contains("NOT a claim"));
assert!(infinite.to_string().contains("is infinite"));
}
#[test]
fn delegating_variants_forward_code_and_remediation() {
let inner = GroupError::DegreeMismatch { left: 3, right: 4 };
let wrapped: FpGroupError = inner.clone().into();
assert_eq!(wrapped.code(), inner.code());
assert_eq!(wrapped.remediation(), inner.remediation());
assert_eq!(wrapped.to_string(), inner.to_string());
let inner = NormalFormError::IncompatibleMultiply {
left_cols: 2,
right_rows: 3,
};
let wrapped: FpGroupError = inner.clone().into();
assert_eq!(wrapped.code(), inner.code());
assert_eq!(wrapped.remediation(), inner.remediation());
}
}