use super::error::FpGroupError;
use std::fmt;
pub const MAX_FREE_RANK: usize = 1024;
pub const MAX_WORD_LETTERS: usize = 10_000_000;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Word {
letters: Vec<i32>,
}
fn free_reduce(letters: &[i32]) -> Vec<i32> {
let mut out: Vec<i32> = Vec::with_capacity(letters.len());
for &l in letters {
if out.last().is_some_and(|&last| last == -l) {
out.pop();
} else {
out.push(l);
}
}
out
}
impl Word {
pub fn identity() -> Word {
Word {
letters: Vec::new(),
}
}
pub fn from_letters(letters: &[i32]) -> Result<Word, FpGroupError> {
for &l in letters {
if l == 0 || l == i32::MIN {
return Err(FpGroupError::InvalidGenerator { letter: l, rank: 0 });
}
}
Ok(Word {
letters: free_reduce(letters),
})
}
pub fn generator(index: usize) -> Result<Word, FpGroupError> {
let letter = i32::try_from(index + 1).map_err(|_| FpGroupError::InvalidPresentation {
detail: format!("generator index {index} does not fit a 32-bit letter"),
})?;
Ok(Word {
letters: vec![letter],
})
}
pub fn commutator(a: &Word, b: &Word) -> Word {
a.times(b).times(&a.inverse()).times(&b.inverse())
}
pub fn letters(&self) -> &[i32] {
&self.letters
}
pub fn len(&self) -> usize {
self.letters.len()
}
pub fn is_empty(&self) -> bool {
self.letters.is_empty()
}
pub fn inverse(&self) -> Word {
Word {
letters: self.letters.iter().rev().map(|l| -l).collect(),
}
}
pub fn times(&self, other: &Word) -> Word {
let mut letters = self.letters.clone();
for &l in &other.letters {
if letters.last().is_some_and(|&last| last == -l) {
letters.pop();
} else {
letters.push(l);
}
}
Word { letters }
}
pub fn pow(&self, exponent: i32) -> Result<Word, FpGroupError> {
if exponent == 0 || self.is_empty() {
return Ok(Word::identity());
}
let base = if exponent < 0 {
self.inverse()
} else {
self.clone()
};
let repeats = exponent.unsigned_abs() as usize;
let wanted = base.len() as u128 * repeats as u128;
if wanted > MAX_WORD_LETTERS as u128 {
return Err(FpGroupError::WordTooLong {
letters: wanted,
max: MAX_WORD_LETTERS,
});
}
let mut letters: Vec<i32> = Vec::with_capacity(base.len().saturating_mul(repeats));
for _ in 0..repeats {
for &l in base.letters() {
if letters.last().is_some_and(|&last| last == -l) {
letters.pop();
} else {
letters.push(l);
}
}
}
Ok(Word { letters })
}
pub fn max_generator(&self) -> Option<usize> {
self.letters
.iter()
.map(|l| l.unsigned_abs() as usize - 1)
.max()
}
pub fn exponent_sums(&self, rank: usize) -> Result<Vec<i64>, FpGroupError> {
let mut sums = vec![0i64; rank];
for &l in &self.letters {
let g = l.unsigned_abs() as usize - 1;
if g >= rank {
return Err(FpGroupError::InvalidGenerator { letter: l, rank });
}
let delta = if l > 0 { 1 } else { -1 };
sums[g] = sums[g]
.checked_add(delta)
.ok_or_else(|| FpGroupError::Internal {
detail: "exponent sum overflowed i64".into(),
})?;
}
Ok(sums)
}
pub fn to_string_with(&self, names: &[String]) -> String {
if self.letters.is_empty() {
return "1".to_string();
}
let name_of = |g: usize| -> String {
names
.get(g)
.cloned()
.unwrap_or_else(|| format!("x{}", g + 1))
};
let mut parts: Vec<String> = Vec::new();
let mut i = 0;
while i < self.letters.len() {
let l = self.letters[i];
let mut run = 1;
while i + run < self.letters.len() && self.letters[i + run] == l {
run += 1;
}
let g = l.unsigned_abs() as usize - 1;
let exp = if l > 0 { run as i64 } else { -(run as i64) };
if exp == 1 {
parts.push(name_of(g));
} else {
parts.push(format!("{}^{}", name_of(g), exp));
}
i += run;
}
parts.join("*")
}
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string_with(&[]))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FreeGroup {
names: Vec<String>,
}
impl FreeGroup {
pub fn new(rank: usize) -> Result<FreeGroup, FpGroupError> {
if rank > MAX_FREE_RANK {
return Err(FpGroupError::InvalidPresentation {
detail: format!("rank {rank} exceeds MAX_FREE_RANK ({MAX_FREE_RANK})"),
});
}
Ok(FreeGroup {
names: (1..=rank).map(|i| format!("x{i}")).collect(),
})
}
pub fn with_names<S: AsRef<str>>(names: &[S]) -> Result<FreeGroup, FpGroupError> {
if names.len() > MAX_FREE_RANK {
return Err(FpGroupError::InvalidPresentation {
detail: format!(
"rank {} exceeds MAX_FREE_RANK ({MAX_FREE_RANK})",
names.len()
),
});
}
let mut out: Vec<String> = Vec::with_capacity(names.len());
for (i, n) in names.iter().enumerate() {
let n = n.as_ref();
if n.is_empty() {
return Err(FpGroupError::InvalidPresentation {
detail: format!("generator {i} has an empty name"),
});
}
let bad = |c: char| {
c.is_whitespace() || matches!(c, '*' | '^' | '(' | ')' | '-' | '+' | ',' | '.')
};
if let Some(c) = n.chars().find(|&c| bad(c)) {
return Err(FpGroupError::InvalidPresentation {
detail: format!("generator name {n:?} contains the reserved character {c:?}"),
});
}
if n.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return Err(FpGroupError::InvalidPresentation {
detail: format!("generator name {n:?} starts with a digit"),
});
}
if out.iter().any(|m| m == n) {
return Err(FpGroupError::InvalidPresentation {
detail: format!("generator name {n:?} is used twice"),
});
}
out.push(n.to_string());
}
Ok(FreeGroup { names: out })
}
pub fn rank(&self) -> usize {
self.names.len()
}
pub fn names(&self) -> &[String] {
&self.names
}
pub fn generator(&self, index: usize) -> Result<Word, FpGroupError> {
if index >= self.rank() {
return Err(FpGroupError::InvalidGenerator {
letter: index as i32 + 1,
rank: self.rank(),
});
}
Word::generator(index)
}
pub fn word(&self, letters: &[i32]) -> Result<Word, FpGroupError> {
let rank = self.rank();
for &l in letters {
if l == 0 || l.unsigned_abs() as usize > rank {
return Err(FpGroupError::InvalidGenerator { letter: l, rank });
}
}
Ok(Word {
letters: free_reduce(letters),
})
}
pub fn format(&self, w: &Word) -> String {
w.to_string_with(&self.names)
}
pub fn parse(&self, input: &str) -> Result<Word, FpGroupError> {
let bytes = input.as_bytes();
let mut pos = 0usize;
let w = self.parse_word(bytes, &mut pos, 0)?;
skip_ws(bytes, &mut pos);
if pos != bytes.len() {
return Err(FpGroupError::WordSyntax {
position: pos,
detail: format!(
"unexpected {:?}",
input[pos..].chars().next().unwrap_or(' ')
),
});
}
Ok(w)
}
fn parse_word(
&self,
bytes: &[u8],
pos: &mut usize,
depth: usize,
) -> Result<Word, FpGroupError> {
if depth > 64 {
return Err(FpGroupError::WordSyntax {
position: *pos,
detail: "parentheses nested more than 64 deep".into(),
});
}
let mut acc = Word::identity();
loop {
skip_ws(bytes, pos);
if *pos >= bytes.len() || bytes[*pos] == b')' {
return Ok(acc);
}
if bytes[*pos] == b'*' {
*pos += 1;
skip_ws(bytes, pos);
if *pos >= bytes.len() || bytes[*pos] == b')' || bytes[*pos] == b'*' {
return Err(FpGroupError::WordSyntax {
position: *pos,
detail: "`*` must be followed by a generator or a sub-word".into(),
});
}
continue;
}
let factor = self.parse_factor(bytes, pos, depth)?;
acc = acc.times(&factor);
}
}
fn parse_factor(
&self,
bytes: &[u8],
pos: &mut usize,
depth: usize,
) -> Result<Word, FpGroupError> {
skip_ws(bytes, pos);
let base = if *pos < bytes.len() && bytes[*pos] == b'(' {
*pos += 1;
let inner = self.parse_word(bytes, pos, depth + 1)?;
skip_ws(bytes, pos);
if *pos >= bytes.len() || bytes[*pos] != b')' {
return Err(FpGroupError::WordSyntax {
position: *pos,
detail: "unclosed `(`".into(),
});
}
*pos += 1;
inner
} else if *pos < bytes.len() && bytes[*pos] == b'1' {
*pos += 1;
Word::identity()
} else {
let rest = &bytes[*pos..];
let mut best: Option<usize> = None;
for (i, n) in self.names.iter().enumerate() {
let nb = n.as_bytes();
let longer_than_best = match best {
None => true,
Some(b) => self.names[b].len() < nb.len(),
};
if rest.len() >= nb.len() && &rest[..nb.len()] == nb && longer_than_best {
best = Some(i);
}
}
match best {
Some(i) => {
*pos += self.names[i].len();
Word::generator(i)?
}
None => {
return Err(FpGroupError::WordSyntax {
position: *pos,
detail: "expected a generator name, `1`, or `(`".into(),
})
}
}
};
skip_ws(bytes, pos);
if *pos < bytes.len() && bytes[*pos] == b'^' {
*pos += 1;
let e = parse_exponent(bytes, pos)?;
return base.pow(e);
}
Ok(base)
}
}
fn skip_ws(bytes: &[u8], pos: &mut usize) {
while *pos < bytes.len() && bytes[*pos].is_ascii_whitespace() {
*pos += 1;
}
}
const MAX_LITERAL_EXPONENT: i64 = 100_000;
fn parse_exponent(bytes: &[u8], pos: &mut usize) -> Result<i32, FpGroupError> {
skip_ws(bytes, pos);
let start = *pos;
let mut sign = 1i64;
if *pos < bytes.len() && (bytes[*pos] == b'-' || bytes[*pos] == b'+') {
if bytes[*pos] == b'-' {
sign = -1;
}
*pos += 1;
}
let digits_start = *pos;
while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
*pos += 1;
}
if *pos == digits_start {
return Err(FpGroupError::WordSyntax {
position: start,
detail: "`^` must be followed by an integer exponent".into(),
});
}
let text =
std::str::from_utf8(&bytes[digits_start..*pos]).map_err(|_| FpGroupError::WordSyntax {
position: start,
detail: "exponent is not valid UTF-8".into(),
})?;
let magnitude: i64 = text.parse().map_err(|_| FpGroupError::WordSyntax {
position: start,
detail: format!("exponent {text} does not fit"),
})?;
if magnitude > MAX_LITERAL_EXPONENT {
return Err(FpGroupError::WordSyntax {
position: start,
detail: format!("exponent {magnitude} is above the literal cap {MAX_LITERAL_EXPONENT}"),
});
}
Ok((sign * magnitude) as i32)
}