use static_assertions::assert_eq_size;
use std::cmp::Ordering;
use std::fmt::Debug;
use ruff_python_trivia::tab_offset_u32;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
pub(super) struct Column(u32);
impl Column {
const fn new(column: u32) -> Self {
Self(column)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
pub(super) struct Character(u32);
impl Character {
const fn new(characters: u32) -> Self {
Self(characters)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub(super) struct Indentation {
column: Column,
character: Character,
}
impl Indentation {
const TAB_SIZE: u32 = 8;
pub(super) const fn root() -> Self {
Self {
column: Column::new(0),
character: Character::new(0),
}
}
#[cfg(test)]
const fn new(column: Column, character: Character) -> Self {
Self { column, character }
}
#[must_use]
pub(super) fn add_space(self) -> Self {
Self {
character: Character(self.character.0 + 1),
column: Column(self.column.0 + 1),
}
}
#[must_use]
pub(super) fn add_tab(self) -> Self {
Self {
character: Character(self.character.0 + 1),
column: Column(self.column.0 + tab_offset_u32(self.column.0, Self::TAB_SIZE)),
}
}
pub(super) fn try_compare(self, other: Indentation) -> Result<Ordering, IndentationError> {
let ordering = self.column.cmp(&other.column);
let consistent = match ordering {
Ordering::Less => self.character < other.character,
Ordering::Equal => self.character == other.character,
Ordering::Greater => true,
};
if consistent {
Ok(ordering)
} else {
Err(IndentationError::InconsistentTabs)
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub(super) enum IndentationError {
InconsistentTabs,
UnmatchedDedent,
TooDeep,
}
#[derive(Debug, Clone, Default)]
pub(super) struct Indentations {
stack: Vec<Indentation>,
}
impl Indentations {
const MAX_DEPTH: usize = 99;
pub(super) fn indent(&mut self, indent: Indentation) -> Result<(), IndentationError> {
debug_assert_eq!(self.current().try_compare(indent), Ok(Ordering::Less));
if self.stack.len() >= Self::MAX_DEPTH {
return Err(IndentationError::TooDeep);
}
self.stack.push(indent);
Ok(())
}
pub(super) fn dedent_one(
&mut self,
new_indentation: Indentation,
) -> Result<Option<Indentation>, IndentationError> {
let previous = self.dedent();
let current = *self.current();
match new_indentation.column.cmp(¤t.column) {
Ordering::Less => Ok(previous),
Ordering::Equal if new_indentation.character == current.character => Ok(previous),
Ordering::Equal => Err(IndentationError::InconsistentTabs),
Ordering::Greater => Err(IndentationError::UnmatchedDedent),
}
}
pub(super) fn dedent(&mut self) -> Option<Indentation> {
self.stack.pop()
}
pub(super) fn current(&self) -> &Indentation {
static ROOT: Indentation = Indentation::root();
self.stack.last().unwrap_or(&ROOT)
}
pub(crate) fn checkpoint(&self) -> IndentationsCheckpoint {
IndentationsCheckpoint(self.stack.clone())
}
pub(crate) fn rewind(&mut self, checkpoint: IndentationsCheckpoint) {
self.stack = checkpoint.0;
}
}
#[derive(Debug, Clone)]
pub(crate) struct IndentationsCheckpoint(Vec<Indentation>);
assert_eq_size!(Indentation, u64);
#[cfg(test)]
mod tests {
use super::{Character, Column, Indentation, IndentationError};
use std::cmp::Ordering;
#[test]
fn indentation_try_compare() {
let tab = Indentation::new(Column::new(8), Character::new(1));
assert_eq!(tab.try_compare(tab), Ok(Ordering::Equal));
let two_tabs = Indentation::new(Column::new(16), Character::new(2));
assert_eq!(two_tabs.try_compare(tab), Ok(Ordering::Greater));
assert_eq!(tab.try_compare(two_tabs), Ok(Ordering::Less));
}
#[test]
fn indentation_try_compare_mixed_tabs() {
let tab = Indentation::new(Column::new(8), Character::new(1));
let eight_spaces = Indentation::new(Column::new(8), Character::new(8));
let tab_and_space = Indentation::new(Column::new(9), Character::new(2));
assert_eq!(
tab.try_compare(eight_spaces),
Err(IndentationError::InconsistentTabs)
);
assert_eq!(
eight_spaces.try_compare(tab_and_space),
Err(IndentationError::InconsistentTabs)
);
assert_eq!(tab.try_compare(tab_and_space), Ok(Ordering::Less));
}
}