use crate::ParserLimits;
use crate::structured_ref;
use crate::tokenizer::{
Associativity, Token, TokenSpan, TokenStream, TokenSubType, TokenType, TokenizerError,
};
use crate::types::{FormulaDialect, ParsingError};
use crate::{ExcelError, LiteralValue};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::hasher::FormulaHasher;
use formualizer_common::coord::{
col_index_from_letters_1based, col_letters_from_1based, parse_a1_1based,
};
use formualizer_common::{
AxisBound, RelativeCoord, SheetCellRef, SheetLocator, SheetRangeRef, SheetRef,
};
use once_cell::sync::Lazy;
use smallvec::SmallVec;
use std::borrow::Cow;
use std::error::Error;
use std::fmt::{self, Display};
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use std::sync::Arc;
type VolatilityFn = dyn Fn(&str) -> bool + Send + Sync + 'static;
type VolatilityClassifierBox = Box<VolatilityFn>;
type VolatilityClassifierArc = Arc<VolatilityFn>;
#[derive(Debug)]
pub struct ParserError {
pub message: String,
pub position: Option<usize>,
}
impl Display for ParserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(pos) = self.position {
write!(f, "ParserError at position {}: {}", pos, self.message)
} else {
write!(f, "ParserError: {}", self.message)
}
}
}
impl Error for ParserError {}
static COLUMN_LOOKUP: Lazy<Vec<String>> = Lazy::new(|| {
let mut cols = Vec::with_capacity(702);
for c in b'A'..=b'Z' {
cols.push(String::from(c as char));
}
for c1 in b'A'..=b'Z' {
for c2 in b'A'..=b'Z' {
cols.push(format!("{}{}", c1 as char, c2 as char));
}
}
cols
});
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum TableSpecifier {
All,
Data,
Headers,
Totals,
Row(TableRowSpecifier),
Column(String),
ColumnRange(String, String),
SpecialItem(SpecialItem),
Combination(Vec<Box<TableSpecifier>>),
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum TableRowSpecifier {
Current,
All,
Data,
Headers,
Totals,
Index(u32),
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum SpecialItem {
Headers,
Data,
Totals,
All,
ThisRow,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct TableReference {
pub name: String,
pub specifier: Option<TableSpecifier>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum ExternalBookRef {
Token(String),
}
impl ExternalBookRef {
pub fn token(&self) -> &str {
match self {
ExternalBookRef::Token(s) => s,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExternalRefKind {
Cell {
row: u32,
col: u32,
row_abs: bool,
col_abs: bool,
},
Range {
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
},
}
impl ExternalRefKind {
pub fn cell(row: u32, col: u32) -> Self {
Self::Cell {
row,
col,
row_abs: false,
col_abs: false,
}
}
pub fn cell_with_abs(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
Self::Cell {
row,
col,
row_abs,
col_abs,
}
}
pub fn range(
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
) -> Self {
Self::Range {
start_row,
start_col,
end_row,
end_col,
start_row_abs: false,
start_col_abs: false,
end_row_abs: false,
end_col_abs: false,
}
}
#[expect(
clippy::too_many_arguments,
reason = "constructor mirroring the enum fields; an explicit signature keeps callers readable"
)]
pub fn range_with_abs(
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
) -> Self {
Self::Range {
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct ExternalReference {
pub raw: String,
pub book: ExternalBookRef,
pub sheet: String,
pub kind: ExternalRefKind,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum ReferenceType {
Cell {
sheet: Option<String>,
row: u32,
col: u32,
row_abs: bool,
col_abs: bool,
},
Range {
sheet: Option<String>,
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
},
Cell3D {
sheet_first: String,
sheet_last: String,
row: u32,
col: u32,
row_abs: bool,
col_abs: bool,
},
Range3D {
sheet_first: String,
sheet_last: String,
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
},
External(ExternalReference),
Table(TableReference),
NamedRange(String),
}
impl Display for TableSpecifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TableSpecifier::All => write!(f, "#All"),
TableSpecifier::Data => write!(f, "#Data"),
TableSpecifier::Headers => write!(f, "#Headers"),
TableSpecifier::Totals => write!(f, "#Totals"),
TableSpecifier::Row(row) => write!(f, "{row}"),
TableSpecifier::Column(column) => write!(f, "{column}"),
TableSpecifier::ColumnRange(start, end) => write!(f, "{start}:{end}"),
TableSpecifier::SpecialItem(item) => write!(f, "{item}"),
TableSpecifier::Combination(specs) => {
let mut first = true;
for spec in specs {
if !first {
write!(f, ",")?;
}
first = false;
match spec.as_ref() {
TableSpecifier::ColumnRange(start, end) => {
write!(f, "[{start}]:[{end}]")?;
}
other => write!(f, "[{other}]")?,
}
}
Ok(())
}
}
}
}
impl Display for TableRowSpecifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TableRowSpecifier::Current => write!(f, "@"),
TableRowSpecifier::All => write!(f, "#All"),
TableRowSpecifier::Data => write!(f, "#Data"),
TableRowSpecifier::Headers => write!(f, "#Headers"),
TableRowSpecifier::Totals => write!(f, "#Totals"),
TableRowSpecifier::Index(idx) => write!(f, "{idx}"),
}
}
}
impl Display for SpecialItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpecialItem::Headers => write!(f, "#Headers"),
SpecialItem::Data => write!(f, "#Data"),
SpecialItem::Totals => write!(f, "#Totals"),
SpecialItem::All => write!(f, "#All"),
SpecialItem::ThisRow => write!(f, "@"),
}
}
}
fn sheet_name_needs_quoting(name: &str) -> bool {
formualizer_common::a1_sheet_name_needs_quoting(name)
}
#[derive(Debug, Clone)]
struct OpenFormulaRefPart {
sheet: Option<String>,
coord: String,
}
type AxisPartWithAbs = Option<(u32, bool)>;
type RangePartWithAbs = (AxisPartWithAbs, AxisPartWithAbs);
#[derive(Debug, Clone)]
enum SheetSpec {
None,
Single(String),
Range { first: String, last: String },
}
impl ReferenceType {
pub fn cell(sheet: Option<String>, row: u32, col: u32) -> Self {
Self::Cell {
sheet,
row,
col,
row_abs: false,
col_abs: false,
}
}
pub fn cell_with_abs(
sheet: Option<String>,
row: u32,
col: u32,
row_abs: bool,
col_abs: bool,
) -> Self {
Self::Cell {
sheet,
row,
col,
row_abs,
col_abs,
}
}
pub fn range(
sheet: Option<String>,
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
) -> Self {
Self::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs: false,
start_col_abs: false,
end_row_abs: false,
end_col_abs: false,
}
}
#[expect(
clippy::too_many_arguments,
reason = "constructor mirroring the enum fields; an explicit signature keeps callers readable"
)]
pub fn range_with_abs(
sheet: Option<String>,
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
) -> Self {
Self::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
}
}
pub fn from_string(reference: &str) -> Result<Self, ParsingError> {
Self::parse_excel_reference(reference)
}
pub fn from_string_with_dialect(
reference: &str,
dialect: FormulaDialect,
) -> Result<Self, ParsingError> {
match dialect {
FormulaDialect::Excel => Self::parse_excel_reference(reference),
FormulaDialect::OpenFormula => Self::parse_openformula_reference(reference)
.or_else(|_| Self::parse_excel_reference(reference)),
}
}
pub fn parse_sheet_ref(reference: &str) -> Result<SheetRef<'static>, ParsingError> {
Self::parse_sheet_ref_with_dialect(reference, FormulaDialect::Excel)
}
pub fn parse_sheet_ref_with_dialect(
reference: &str,
dialect: FormulaDialect,
) -> Result<SheetRef<'static>, ParsingError> {
match dialect {
FormulaDialect::Excel => Self::parse_excel_sheet_ref(reference),
FormulaDialect::OpenFormula => Self::parse_openformula_sheet_ref(reference)
.or_else(|_| Self::parse_excel_sheet_ref(reference)),
}
}
pub fn to_sheet_ref_lossy(&self) -> Option<SheetRef<'_>> {
match self {
ReferenceType::Cell {
sheet,
row,
col,
row_abs,
col_abs,
} => {
let row0 = row.checked_sub(1)?;
let col0 = col.checked_sub(1)?;
let sheet_loc = match sheet.as_deref() {
Some(name) => SheetLocator::from_name(name),
None => SheetLocator::Current,
};
let coord = RelativeCoord::new(row0, col0, *row_abs, *col_abs);
Some(SheetRef::Cell(SheetCellRef::new(sheet_loc, coord)))
}
ReferenceType::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => {
let sheet_loc = match sheet.as_deref() {
Some(name) => SheetLocator::from_name(name),
None => SheetLocator::Current,
};
let sr = start_row
.and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *start_row_abs)));
if start_row.is_some() && sr.is_none() {
return None;
}
let sc = start_col
.and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *start_col_abs)));
if start_col.is_some() && sc.is_none() {
return None;
}
let er =
end_row.and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *end_row_abs)));
if end_row.is_some() && er.is_none() {
return None;
}
let ec =
end_col.and_then(|v| v.checked_sub(1).map(|i| AxisBound::new(i, *end_col_abs)));
if end_col.is_some() && ec.is_none() {
return None;
}
let range = SheetRangeRef::from_parts(sheet_loc, sr, sc, er, ec).ok()?;
Some(SheetRef::Range(range))
}
_ => None,
}
}
fn parse_excel_sheet_ref(reference: &str) -> Result<SheetRef<'static>, ParsingError> {
let (spec, ref_part) = Self::extract_sheet_spec(reference);
let sheet = match spec {
SheetSpec::None => None,
SheetSpec::Single(name) => Some(name),
SheetSpec::Range { .. } => {
return Err(ParsingError::InvalidReference(
"3D references are not supported for SheetRef".to_string(),
));
}
};
if ref_part.contains('[') {
return Err(ParsingError::InvalidReference(
"Table references are not supported for SheetRef".to_string(),
));
}
let sheet_loc: SheetLocator<'static> = match sheet {
Some(name) => SheetLocator::from_name(name),
None => SheetLocator::Current,
};
if let Some((start, end)) = ref_part.split_once(':') {
let (start_col, start_row) = Self::parse_range_part_with_abs(start)?;
let (end_col, end_row) = Self::parse_range_part_with_abs(end)?;
let start_col = Self::axis_bound_from_1based(start_col)?;
let start_row = Self::axis_bound_from_1based(start_row)?;
let end_col = Self::axis_bound_from_1based(end_col)?;
let end_row = Self::axis_bound_from_1based(end_row)?;
let range =
SheetRangeRef::from_parts(sheet_loc, start_row, start_col, end_row, end_col)
.map_err(|err| ParsingError::InvalidReference(err.to_string()))?;
Ok(SheetRef::Range(range))
} else {
let (row, col, row_abs, col_abs) = parse_a1_1based(&ref_part)
.map_err(|err| ParsingError::InvalidReference(err.to_string()))?;
#[expect(
clippy::arithmetic_side_effects,
reason = "parse_a1_1based returns row >= 1 and col >= 1"
)]
let coord = RelativeCoord::new(row - 1, col - 1, row_abs, col_abs);
Ok(SheetRef::Cell(SheetCellRef::new(sheet_loc, coord)))
}
}
fn parse_openformula_sheet_ref(reference: &str) -> Result<SheetRef<'static>, ParsingError> {
Self::parse_excel_sheet_ref(reference)
}
fn axis_bound_from_1based(
bound: Option<(u32, bool)>,
) -> Result<Option<AxisBound>, ParsingError> {
match bound {
Some((index, abs)) => AxisBound::from_excel_1based(index, abs)
.map(Some)
.map_err(|err| ParsingError::InvalidReference(err.to_string())),
None => Ok(None),
}
}
#[expect(
clippy::arithmetic_side_effects,
clippy::string_slice,
reason = "cursor bounded by slice length; the column and row runs are sliced at ASCII positions found by the scan"
)]
fn parse_range_part_with_abs(part: &str) -> Result<RangePartWithAbs, ParsingError> {
if let Ok((row, col, row_abs, col_abs)) = parse_a1_1based(part) {
return Ok((Some((col, col_abs)), Some((row, row_abs))));
}
let bytes = part.as_bytes();
let len = bytes.len();
let mut i = 0usize;
let mut col_abs = false;
let mut row_abs = false;
if bytes.get(i) == Some(&b'$') {
col_abs = true;
i += 1;
}
let col_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_alphabetic) {
i += 1;
}
if i > col_start {
let col_str = &part[col_start..i];
let col1 = Self::column_to_number(col_str)?;
if i == len {
return Ok((Some((col1, col_abs)), None));
}
if bytes.get(i) == Some(&b'$') {
row_abs = true;
i += 1;
}
if i >= len {
return Err(ParsingError::InvalidReference(format!(
"Invalid range part: {part}"
)));
}
let row_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if row_start == i || i != len {
return Err(ParsingError::InvalidReference(format!(
"Invalid range part: {part}"
)));
}
let row_str = &part[row_start..i];
let row1 = row_str
.parse::<u32>()
.map_err(|_| ParsingError::InvalidReference(format!("Invalid row: {row_str}")))?;
if row1 == 0 {
return Err(ParsingError::InvalidReference(format!(
"Invalid range part: {part}"
)));
}
return Ok((Some((col1, col_abs)), Some((row1, row_abs))));
}
i = 0;
if bytes.get(i) == Some(&b'$') {
row_abs = true;
i += 1;
}
let row_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if row_start == i || i != len {
return Err(ParsingError::InvalidReference(format!(
"Invalid range part: {part}"
)));
}
let row_str = &part[row_start..i];
let row1 = row_str
.parse::<u32>()
.map_err(|_| ParsingError::InvalidReference(format!("Invalid row: {row_str}")))?;
if row1 == 0 {
return Err(ParsingError::InvalidReference(format!(
"Invalid range part: {part}"
)));
}
Ok((None, Some((row1, row_abs))))
}
fn parse_3d_reference(first: &str, last: &str, ref_part: &str) -> Result<Self, ParsingError> {
if first.is_empty() || last.is_empty() {
return Err(ParsingError::InvalidReference(format!(
"3D reference requires two sheet names: {first}:{last}!{ref_part}"
)));
}
if ref_part.is_empty() {
return Err(ParsingError::InvalidReference(format!(
"3D reference {first}:{last}! is missing a cell or range"
)));
}
if ref_part.contains('[') {
return Err(ParsingError::InvalidReference(format!(
"3D reference {first}:{last}!{ref_part} cannot target a table"
)));
}
if let Some((start, end)) = ref_part.split_once(':') {
let (start_col, start_row) = Self::parse_range_part_with_abs(start)?;
let (end_col, end_row) = Self::parse_range_part_with_abs(end)?;
let split = |bound: Option<(u32, bool)>| match bound {
Some((index, abs)) => (Some(index), abs),
None => (None, false),
};
let (start_col, start_col_abs) = split(start_col);
let (start_row, start_row_abs) = split(start_row);
let (end_col, end_col_abs) = split(end_col);
let (end_row, end_row_abs) = split(end_row);
Ok(ReferenceType::Range3D {
sheet_first: first.to_string(),
sheet_last: last.to_string(),
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
})
} else {
let (col, row, col_abs, row_abs) =
Self::parse_cell_reference(ref_part).map_err(|_| {
ParsingError::InvalidReference(format!(
"Invalid 3D reference target: {ref_part}"
))
})?;
Ok(ReferenceType::Cell3D {
sheet_first: first.to_string(),
sheet_last: last.to_string(),
row,
col,
row_abs,
col_abs,
})
}
}
#[inline]
fn strip_workbook_zero_prefixes(reference: &str) -> Cow<'_, str> {
if !reference.starts_with("[0]") && !reference.starts_with("'[0]") {
return Cow::Borrowed(reference);
}
Self::strip_workbook_zero_prefixes_slow(reference)
}
#[cold]
#[inline(never)]
fn strip_workbook_zero_prefixes_slow(reference: &str) -> Cow<'_, str> {
let Some(after_last_bang) = reference.rsplit_once('!').map(|(_, after)| after.len()) else {
return Cow::Borrowed(reference);
};
let has_bang = |tail: &str| tail.len() > after_last_bang;
let mut tail = reference;
let mut quoted = false;
loop {
if !quoted
&& let Some(rest) = tail.strip_prefix("[0]")
&& has_bang(rest)
{
tail = rest.strip_prefix('!').unwrap_or(rest);
} else if !quoted
&& let Some(rest) = tail.strip_prefix("'[0]")
&& has_bang(rest)
{
quoted = true;
tail = rest;
} else if quoted
&& let Some(rest) = tail.strip_prefix("[0]")
&& has_bang(rest)
{
tail = rest;
} else {
break;
}
}
if quoted {
Cow::Owned(format!("'{tail}"))
} else {
Cow::Borrowed(tail)
}
}
fn parse_excel_reference(reference: &str) -> Result<Self, ParsingError> {
let reference = Self::strip_workbook_zero_prefixes(reference);
let reference = reference.as_ref();
if reference.starts_with('[') && reference.ends_with(']') && !reference.contains('!') {
return Self::parse_bracketed_structured_reference(reference);
}
let (sheet_spec, ref_part) = Self::extract_sheet_spec(reference);
let sheet = match sheet_spec {
SheetSpec::None => None,
SheetSpec::Single(name) => Some(name),
SheetSpec::Range { first, last } => {
return Self::parse_3d_reference(&first, &last, &ref_part);
}
};
if ref_part.contains('[') {
if Self::is_r1c1_shape(&ref_part) {
return Ok(ReferenceType::NamedRange(reference.to_string()));
}
return Self::parse_table_reference(&ref_part);
}
let external_sheet = sheet.as_deref().and_then(|s| {
let lb = s.rfind('[')?;
let (_, sheet_name) = s.get(lb..)?.split_once(']')?;
let token = s.strip_suffix(sheet_name)?;
if sheet_name.is_empty() {
None
} else {
Some((token, sheet_name))
}
});
if let Some((start, end)) = ref_part.split_once(':') {
let (start_col, start_row) = Self::parse_range_part_with_abs(start)?;
let (end_col, end_row) = Self::parse_range_part_with_abs(end)?;
let split = |bound: Option<(u32, bool)>| match bound {
Some((index, abs)) => (Some(index), abs),
None => (None, false),
};
let (start_col, start_col_abs) = split(start_col);
let (start_row, start_row_abs) = split(start_row);
let (end_col, end_col_abs) = split(end_col);
let (end_row, end_row_abs) = split(end_row);
if let Some((book_token, sheet_name)) = external_sheet {
Ok(ReferenceType::External(ExternalReference {
raw: reference.to_string(),
book: ExternalBookRef::Token(book_token.to_string()),
sheet: sheet_name.to_string(),
kind: ExternalRefKind::Range {
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
},
}))
} else {
Ok(ReferenceType::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
})
}
} else {
match Self::parse_cell_reference(&ref_part) {
Ok((col, row, col_abs, row_abs)) => {
if let Some((book_token, sheet_name)) = external_sheet {
Ok(ReferenceType::External(ExternalReference {
raw: reference.to_string(),
book: ExternalBookRef::Token(book_token.to_string()),
sheet: sheet_name.to_string(),
kind: ExternalRefKind::Cell {
row,
col,
row_abs,
col_abs,
},
}))
} else {
Ok(ReferenceType::Cell {
sheet,
row,
col,
row_abs,
col_abs,
})
}
}
Err(_) => {
Ok(ReferenceType::NamedRange(reference.to_string()))
}
}
}
}
fn parse_cell_reference(reference: &str) -> Result<(u32, u32, bool, bool), ParsingError> {
parse_a1_1based(reference)
.map(|(row, col, row_abs, col_abs)| (col, row, col_abs, row_abs))
.map_err(|_| {
ParsingError::InvalidReference(format!("Invalid cell reference: {reference}"))
})
}
pub(crate) fn column_to_number(column: &str) -> Result<u32, ParsingError> {
col_index_from_letters_1based(column)
.map_err(|_| ParsingError::InvalidReference(format!("Invalid column: {column}")))
}
pub(crate) fn number_to_column(num: u32) -> String {
if num == 0 {
return String::new();
}
if let Some(letters) = num
.checked_sub(1)
.and_then(|index| COLUMN_LOOKUP.get(index as usize))
{
return letters.clone();
}
col_letters_from_1based(num).unwrap_or_default()
}
fn format_col(col: u32, abs: bool) -> String {
if abs {
format!("${}", Self::number_to_column(col))
} else {
Self::number_to_column(col)
}
}
fn format_row(row: u32, abs: bool) -> String {
if abs {
format!("${row}")
} else {
row.to_string()
}
}
}
impl Display for ReferenceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
ReferenceType::Cell {
sheet,
row,
col,
row_abs,
col_abs,
} => {
let col_str = Self::format_col(*col, *col_abs);
let row_str = Self::format_row(*row, *row_abs);
if let Some(sheet_name) = sheet {
format!(
"{}!{col_str}{row_str}",
formualizer_common::format_a1_sheet_name(sheet_name)
)
} else {
format!("{col_str}{row_str}")
}
}
ReferenceType::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => {
let start_ref = match (start_col, start_row) {
(Some(col), Some(row)) => format!(
"{}{}",
Self::format_col(*col, *start_col_abs),
Self::format_row(*row, *start_row_abs)
),
(Some(col), None) => Self::format_col(*col, *start_col_abs),
(None, Some(row)) => Self::format_row(*row, *start_row_abs),
(None, None) => "".to_string(), };
let end_ref = match (end_col, end_row) {
(Some(col), Some(row)) => format!(
"{}{}",
Self::format_col(*col, *end_col_abs),
Self::format_row(*row, *end_row_abs)
),
(Some(col), None) => Self::format_col(*col, *end_col_abs),
(None, Some(row)) => Self::format_row(*row, *end_row_abs),
(None, None) => "".to_string(), };
let range_part = format!("{start_ref}:{end_ref}");
if let Some(sheet_name) = sheet {
format!(
"{}!{range_part}",
formualizer_common::format_a1_sheet_name(sheet_name)
)
} else {
range_part
}
}
ReferenceType::Cell3D {
sheet_first,
sheet_last,
row,
col,
row_abs,
col_abs,
} => {
let col_str = Self::format_col(*col, *col_abs);
let row_str = Self::format_row(*row, *row_abs);
let prefix = format_3d_sheet_prefix(sheet_first, sheet_last);
format!("{prefix}!{col_str}{row_str}")
}
ReferenceType::Range3D {
sheet_first,
sheet_last,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => {
let start_ref = match (start_col, start_row) {
(Some(col), Some(row)) => format!(
"{}{}",
Self::format_col(*col, *start_col_abs),
Self::format_row(*row, *start_row_abs)
),
(Some(col), None) => Self::format_col(*col, *start_col_abs),
(None, Some(row)) => Self::format_row(*row, *start_row_abs),
(None, None) => "".to_string(),
};
let end_ref = match (end_col, end_row) {
(Some(col), Some(row)) => format!(
"{}{}",
Self::format_col(*col, *end_col_abs),
Self::format_row(*row, *end_row_abs)
),
(Some(col), None) => Self::format_col(*col, *end_col_abs),
(None, Some(row)) => Self::format_row(*row, *end_row_abs),
(None, None) => "".to_string(),
};
let range_part = format!("{start_ref}:{end_ref}");
let prefix = format_3d_sheet_prefix(sheet_first, sheet_last);
format!("{prefix}!{range_part}")
}
ReferenceType::External(ext) => ext.raw.clone(),
ReferenceType::Table(table_ref) => {
if let Some(specifier) = &table_ref.specifier {
match specifier {
TableSpecifier::Column(column) => {
format!("{}[{}]", table_ref.name, column.trim())
}
TableSpecifier::ColumnRange(start, end) => {
format!("{}[{}:{}]", table_ref.name, start.trim(), end.trim())
}
_ => {
format!("{}[{}]", table_ref.name, specifier)
}
}
} else {
table_ref.name.clone()
}
}
ReferenceType::NamedRange(name) => name.clone(),
}
)
}
}
fn format_3d_sheet_prefix(first: &str, last: &str) -> String {
if sheet_name_needs_quoting(first) || sheet_name_needs_quoting(last) {
let escaped = format!("{first}:{last}").replace('\'', "''");
format!("'{escaped}'")
} else {
format!("{first}:{last}")
}
}
impl TryFrom<&str> for ReferenceType {
type Error = ParsingError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
ReferenceType::from_string(value)
}
}
impl FromStr for ReferenceType {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ReferenceType::from_string(s)
}
}
impl ReferenceType {
pub fn normalise(&self) -> String {
format!("{self}")
}
#[expect(
clippy::arithmetic_side_effects,
clippy::string_slice,
reason = "cursor bounded by slice length; the name is sliced between ASCII quotes or up to an ASCII delimiter"
)]
fn read_sheet_segment(reference: &str, start: usize) -> Option<(String, usize, bool)> {
let bytes = reference.as_bytes();
let first = *bytes.get(start)?;
if first == b'\'' {
let mut i = start + 1;
let body_start = i;
while let Some(&b) = bytes.get(i) {
if b == b'\'' {
if bytes.get(i + 1) == Some(&b'\'') {
i += 2;
continue;
}
let raw = &reference[body_start..i];
let name = raw.replace("''", "'");
return Some((name, i + 1, true));
}
i += 1;
}
None
} else {
let mut i = start;
while let Some(&b) = bytes.get(i) {
match b {
b':' | b'!' | b'\'' | b' ' | b'\t' | b'\n' | b'\r' => break,
_ => i += 1,
}
}
if i == start {
None
} else {
Some((reference[start..i].to_string(), i, false))
}
}
}
#[expect(
clippy::arithmetic_side_effects,
clippy::string_slice,
reason = "byte offset from ASCII delimiter search: after_first, after_second and bang index ASCII `:` or `!` bytes inside reference"
)]
fn extract_sheet_spec(reference: &str) -> (SheetSpec, String) {
let Some((first_name, after_first, first_quoted)) = Self::read_sheet_segment(reference, 0)
else {
return Self::extract_sheet_spec_fallback(reference);
};
let bytes = reference.as_bytes();
if bytes.get(after_first) == Some(&b':') {
let second_start = after_first + 1;
if let Some((second_name, after_second, _)) =
Self::read_sheet_segment(reference, second_start)
&& bytes.get(after_second) == Some(&b'!')
{
let ref_part = reference[after_second + 1..].to_string();
return (
SheetSpec::Range {
first: first_name,
last: second_name,
},
ref_part,
);
}
if let Some(bang) = reference
.get(second_start..)
.and_then(|rest| rest.find('!'))
{
let ref_part = reference[second_start + bang + 1..].to_string();
return (
SheetSpec::Range {
first: first_name,
last: String::new(),
},
ref_part,
);
}
}
if bytes.get(after_first) == Some(&b'!') {
let ref_part = reference[after_first + 1..].to_string();
if first_quoted
&& !first_name.contains('[')
&& let Some((first, last)) = first_name.split_once(':')
&& !first.is_empty()
&& !last.is_empty()
&& !last.contains(':')
{
return (
SheetSpec::Range {
first: first.to_string(),
last: last.to_string(),
},
ref_part,
);
}
return (SheetSpec::Single(first_name), ref_part);
}
Self::extract_sheet_spec_fallback(reference)
}
fn extract_sheet_spec_fallback(reference: &str) -> (SheetSpec, String) {
let mut chars = reference.chars();
if chars.next().is_some()
&& let Some((_, ref_part)) = chars.as_str().split_once('!')
&& let Some(sheet) = reference
.strip_suffix(ref_part)
.and_then(|head| head.strip_suffix('!'))
{
return (SheetSpec::Single(sheet.to_string()), ref_part.to_string());
}
(SheetSpec::None, reference.to_string())
}
#[expect(
clippy::arithmetic_side_effects,
reason = "cursor bounded by slice length"
)]
fn is_r1c1_shape(s: &str) -> bool {
let bytes = s.as_bytes();
let len = bytes.len();
let mut i = 0usize;
let mut anchored = false;
if bytes.get(i) != Some(&b'R') {
return false;
}
i += 1;
let row_digits_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i > row_digits_start {
anchored = true;
}
if bytes.get(i) == Some(&b'[') {
i += 1;
if bytes.get(i) == Some(&b'-') {
i += 1;
}
let n_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == n_start || bytes.get(i) != Some(&b']') {
return false;
}
i += 1;
anchored = true;
}
if bytes.get(i) != Some(&b'C') {
return false;
}
i += 1;
let col_digits_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i > col_digits_start {
anchored = true;
}
if bytes.get(i) == Some(&b'[') {
i += 1;
if bytes.get(i) == Some(&b'-') {
i += 1;
}
let n_start = i;
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == n_start || bytes.get(i) != Some(&b']') {
return false;
}
i += 1;
anchored = true;
}
i == len && anchored
}
fn parse_table_reference(reference: &str) -> Result<Self, ParsingError> {
let (table_name, specifier_str) = reference
.find('[')
.and_then(|bracket_pos| reference.split_at_checked(bracket_pos))
.ok_or_else(|| {
ParsingError::InvalidReference(format!(
"Missing '[' in table reference: {reference}"
))
})?;
let table_name = table_name.trim();
if table_name.is_empty() {
return Err(ParsingError::InvalidReference(reference.to_string()));
}
let specifier = structured_ref::parse_full_specifier(specifier_str)?;
Ok(ReferenceType::Table(TableReference {
name: table_name.to_string(),
specifier,
}))
}
fn parse_bracketed_structured_reference(reference: &str) -> Result<Self, ParsingError> {
debug_assert!(reference.starts_with('[') && reference.ends_with(']'));
let specifier = structured_ref::parse_full_specifier(reference)?;
match specifier {
Some(TableSpecifier::Column(name)) => Ok(ReferenceType::Table(TableReference {
name,
specifier: Some(TableSpecifier::SpecialItem(SpecialItem::Data)),
})),
other => Ok(ReferenceType::Table(TableReference {
name: String::new(),
specifier: other,
})),
}
}
fn parse_openformula_reference(reference: &str) -> Result<Self, ParsingError> {
if let Some(inner) = reference
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
{
if inner.is_empty() {
return Err(ParsingError::InvalidReference(
"Empty OpenFormula reference".to_string(),
));
}
let (start_part_str, end_part_str) = match inner.split_once(':') {
Some((start, end)) => (start, Some(end)),
None => (inner, None),
};
let start_part = Self::parse_openformula_part(start_part_str)?;
let end_part = if let Some(part) = end_part_str {
Some(Self::parse_openformula_part(part)?)
} else {
None
};
let sheet = match (&start_part.sheet, &end_part) {
(Some(sheet), Some(end)) => {
if let Some(end_sheet) = &end.sheet
&& end_sheet != sheet
{
return Err(ParsingError::InvalidReference(format!(
"Mismatched sheets in reference: {sheet} vs {end_sheet}"
)));
}
Some(sheet.clone())
}
(Some(sheet), None) => Some(sheet.clone()),
(None, Some(end)) => end.sheet.clone(),
(None, None) => None,
};
let mut excel_like = String::new();
if let Some(sheet_name) = sheet {
if sheet_name_needs_quoting(&sheet_name) {
let escaped = sheet_name.replace('\'', "''");
excel_like.push('\'');
excel_like.push_str(&escaped);
excel_like.push('\'');
} else {
excel_like.push_str(&sheet_name);
}
excel_like.push('!');
}
excel_like.push_str(&start_part.coord);
if let Some(end) = end_part {
excel_like.push(':');
excel_like.push_str(&end.coord);
}
return Self::parse_excel_reference(&excel_like);
}
Err(ParsingError::InvalidReference(format!(
"Unsupported OpenFormula reference: {reference}"
)))
}
fn parse_openformula_part(part: &str) -> Result<OpenFormulaRefPart, ParsingError> {
let trimmed = part.trim();
if trimmed.is_empty() {
return Err(ParsingError::InvalidReference(
"Empty component in OpenFormula reference".to_string(),
));
}
if trimmed == "." {
return Err(ParsingError::InvalidReference(
"Incomplete OpenFormula reference component".to_string(),
));
}
if trimmed.starts_with('[') {
return Err(ParsingError::InvalidReference(format!(
"Unexpected '[' in OpenFormula reference component: {trimmed}"
)));
}
let (sheet, coord_slice) = if let Some(stripped) = trimmed.strip_prefix('.') {
(None, stripped.trim())
} else if let Some(dot_idx) = Self::find_openformula_sheet_separator(trimmed) {
#[expect(
clippy::string_slice,
clippy::arithmetic_side_effects,
reason = "byte offset from ASCII delimiter search: dot_idx is the index of a `.`"
)]
let (sheet_part, coord_part) = (&trimmed[..dot_idx], &trimmed[dot_idx + 1..]);
let (sheet_part, coord_part) = (sheet_part.trim(), coord_part.trim());
if coord_part.is_empty() {
return Err(ParsingError::InvalidReference(format!(
"Missing coordinate in OpenFormula reference component: {trimmed}"
)));
}
let sheet_name = Self::normalise_openformula_sheet(sheet_part)?;
(Some(sheet_name), coord_part)
} else {
(None, trimmed)
};
let coord = coord_slice.trim_start_matches('.').trim().to_string();
if coord.is_empty() {
return Err(ParsingError::InvalidReference(format!(
"Missing coordinate in OpenFormula reference component: {trimmed}"
)));
}
Ok(OpenFormulaRefPart { sheet, coord })
}
fn normalise_openformula_sheet(sheet: &str) -> Result<String, ParsingError> {
let without_abs = sheet.trim().trim_start_matches('$');
if let Some(quoted) = without_abs.strip_prefix('\'') {
let Some(inner) = quoted.strip_suffix('\'') else {
return Err(ParsingError::InvalidReference(format!(
"Unterminated sheet name in OpenFormula reference: {sheet}"
)));
};
Ok(inner.replace("''", "'"))
} else {
Ok(without_abs.to_string())
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "cursor bounded by slice length"
)]
fn find_openformula_sheet_separator(part: &str) -> Option<usize> {
let bytes = part.as_bytes();
let mut i = 0;
let mut in_quotes = false;
while let Some(&b) = bytes.get(i) {
match b {
b'\'' => {
if bytes.get(i + 1) == Some(&b'\'') {
i += 2;
continue;
}
in_quotes = !in_quotes;
i += 1;
}
b'.' if !in_quotes => return Some(i),
_ => i += 1,
}
}
None
}
pub fn to_excel_string(&self) -> String {
match self {
ReferenceType::Cell {
sheet,
row,
col,
row_abs,
col_abs,
} => {
let col_str = Self::format_col(*col, *col_abs);
let row_str = Self::format_row(*row, *row_abs);
if let Some(s) = sheet {
if sheet_name_needs_quoting(s) {
let escaped_name = s.replace('\'', "''");
format!("'{}'!{}{}", escaped_name, col_str, row_str)
} else {
format!("{}!{}{}", s, col_str, row_str)
}
} else {
format!("{}{}", col_str, row_str)
}
}
ReferenceType::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => {
let start_ref = match (start_col, start_row) {
(Some(col), Some(row)) => format!(
"{}{}",
Self::format_col(*col, *start_col_abs),
Self::format_row(*row, *start_row_abs)
),
(Some(col), None) => Self::format_col(*col, *start_col_abs),
(None, Some(row)) => Self::format_row(*row, *start_row_abs),
(None, None) => "".to_string(), };
let end_ref = match (end_col, end_row) {
(Some(col), Some(row)) => format!(
"{}{}",
Self::format_col(*col, *end_col_abs),
Self::format_row(*row, *end_row_abs)
),
(Some(col), None) => Self::format_col(*col, *end_col_abs),
(None, Some(row)) => Self::format_row(*row, *end_row_abs),
(None, None) => "".to_string(), };
let range_part = format!("{start_ref}:{end_ref}");
if let Some(s) = sheet {
if sheet_name_needs_quoting(s) {
let escaped_name = s.replace('\'', "''");
format!("'{escaped_name}'!{range_part}")
} else {
format!("{s}!{range_part}")
}
} else {
range_part
}
}
ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => format!("{self}"),
ReferenceType::External(ext) => ext.raw.clone(),
ReferenceType::Table(table_ref) => {
if let Some(specifier) = &table_ref.specifier {
format!("{}[{}]", table_ref.name, specifier)
} else {
table_ref.name.clone()
}
}
ReferenceType::NamedRange(name) => name.clone(),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum UnaryOperator {
Plus,
Minus,
Percent,
Spill,
ImplicitIntersection,
}
#[cfg(feature = "serde")]
impl Serialize for UnaryOperator {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for UnaryOperator {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::try_from(value.as_str()).map_err(serde::de::Error::custom)
}
}
impl UnaryOperator {
pub const fn as_str(self) -> &'static str {
match self {
Self::Plus => "+",
Self::Minus => "-",
Self::Percent => "%",
Self::Spill => "#",
Self::ImplicitIntersection => "@",
}
}
}
impl fmt::Debug for UnaryOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.as_str(), f)
}
}
impl Display for UnaryOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for UnaryOperator {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"+" => Ok(Self::Plus),
"-" => Ok(Self::Minus),
"%" => Ok(Self::Percent),
"#" => Ok(Self::Spill),
"@" => Ok(Self::ImplicitIntersection),
_ => Err("unsupported formula operator"),
}
}
}
impl std::hash::Hash for UnaryOperator {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum BinaryOperator {
Add,
Subtract,
Multiply,
Divide,
Power,
Concat,
Equal,
NotEqual,
Less,
Greater,
LessEqual,
GreaterEqual,
Range,
Union,
Intersection,
}
#[cfg(feature = "serde")]
impl Serialize for BinaryOperator {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for BinaryOperator {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::try_from(value.as_str()).map_err(serde::de::Error::custom)
}
}
impl BinaryOperator {
pub const fn as_str(self) -> &'static str {
match self {
Self::Add => "+",
Self::Subtract => "-",
Self::Multiply => "*",
Self::Divide => "/",
Self::Power => "^",
Self::Concat => "&",
Self::Equal => "=",
Self::NotEqual => "<>",
Self::Less => "<",
Self::Greater => ">",
Self::LessEqual => "<=",
Self::GreaterEqual => ">=",
Self::Range => ":",
Self::Union => ",",
Self::Intersection => " ",
}
}
}
impl fmt::Debug for BinaryOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.as_str(), f)
}
}
impl Display for BinaryOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for BinaryOperator {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"+" => Ok(Self::Add),
"-" => Ok(Self::Subtract),
"*" => Ok(Self::Multiply),
"/" => Ok(Self::Divide),
"^" => Ok(Self::Power),
"&" => Ok(Self::Concat),
"=" => Ok(Self::Equal),
"<>" => Ok(Self::NotEqual),
"<" => Ok(Self::Less),
">" => Ok(Self::Greater),
"<=" => Ok(Self::LessEqual),
">=" => Ok(Self::GreaterEqual),
":" => Ok(Self::Range),
"," => Ok(Self::Union),
" " => Ok(Self::Intersection),
_ => Err("unsupported formula operator"),
}
}
}
impl std::hash::Hash for BinaryOperator {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum ASTNodeType {
Literal(LiteralValue),
Omitted,
Reference {
original: String, reference: ReferenceType, },
UnaryOp {
op: UnaryOperator,
expr: Box<ASTNode>,
},
BinaryOp {
op: BinaryOperator,
left: Box<ASTNode>,
right: Box<ASTNode>,
},
Function {
name: String,
args: Vec<ASTNode>, },
Call {
callee: Box<ASTNode>,
args: Vec<ASTNode>,
},
Array(Vec<Vec<ASTNode>>), }
impl Display for ASTNodeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ASTNodeType::Literal(value) => write!(f, "Literal({value})"),
ASTNodeType::Omitted => write!(f, "Omitted"),
ASTNodeType::Reference { reference, .. } => write!(f, "Reference({reference:?})"),
ASTNodeType::UnaryOp { op, expr } => write!(f, "UnaryOp({op}, {expr})"),
ASTNodeType::BinaryOp { op, left, right } => {
write!(f, "BinaryOp({op}, {left}, {right})")
}
ASTNodeType::Function { name, args } => write!(f, "Function({name}, {args:?})"),
ASTNodeType::Call { callee, args } => write!(f, "Call({callee}, {args:?})"),
ASTNodeType::Array(rows) => write!(f, "Array({rows:?})"),
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ASTNode {
pub node_type: ASTNodeType,
pub source_token: Option<Token>,
pub contains_volatile: bool,
}
impl ASTNode {
pub fn new(node_type: ASTNodeType, source_token: Option<Token>) -> Self {
ASTNode {
node_type,
source_token,
contains_volatile: false,
}
}
pub fn new_with_volatile(
node_type: ASTNodeType,
source_token: Option<Token>,
contains_volatile: bool,
) -> Self {
ASTNode {
node_type,
source_token,
contains_volatile,
}
}
pub fn contains_volatile(&self) -> bool {
self.contains_volatile
}
pub fn fingerprint(&self) -> u64 {
self.calculate_hash()
}
pub fn calculate_hash(&self) -> u64 {
let mut hasher = FormulaHasher::new();
self.hash_node(&mut hasher);
hasher.finish()
}
fn hash_node(&self, hasher: &mut FormulaHasher) {
match &self.node_type {
ASTNodeType::Literal(value) => {
hasher.write(&[1]); value.hash(hasher);
}
ASTNodeType::Omitted => hasher.write(&[8]),
ASTNodeType::Reference { reference, .. } => {
hasher.write(&[2]); reference.hash(hasher);
}
ASTNodeType::UnaryOp { op, expr } => {
hasher.write(&[3]); hasher.write(op.as_str().as_bytes());
expr.hash_node(hasher);
}
ASTNodeType::BinaryOp { op, left, right } => {
hasher.write(&[4]); hasher.write(op.as_str().as_bytes());
left.hash_node(hasher);
right.hash_node(hasher);
}
ASTNodeType::Function { name, args } => {
hasher.write(&[5]); let name_lower = name.to_lowercase();
hasher.write(name_lower.as_bytes());
hasher.write_usize(args.len());
for arg in args {
arg.hash_node(hasher);
}
}
ASTNodeType::Call { callee, args } => {
hasher.write(&[7]); callee.hash_node(hasher);
hasher.write_usize(args.len());
for arg in args {
arg.hash_node(hasher);
}
}
ASTNodeType::Array(rows) => {
hasher.write(&[6]); hasher.write_usize(rows.len());
for row in rows {
hasher.write_usize(row.len());
for item in row {
item.hash_node(hasher);
}
}
}
}
}
pub fn get_dependencies(&self) -> Vec<&ReferenceType> {
let mut dependencies = Vec::new();
self.collect_dependencies(&mut dependencies);
dependencies
}
pub fn get_dependency_strings(&self) -> Vec<String> {
self.get_dependencies()
.into_iter()
.map(|dep| format!("{dep}"))
.collect()
}
fn collect_dependencies<'a>(&'a self, dependencies: &mut Vec<&'a ReferenceType>) {
let mut stack: Vec<&'a ASTNode> = vec![self];
while let Some(node) = stack.pop() {
match &node.node_type {
ASTNodeType::Reference { reference, .. } => dependencies.push(reference),
ASTNodeType::UnaryOp { expr, .. } => stack.push(expr),
ASTNodeType::BinaryOp { left, right, .. } => {
stack.push(right);
stack.push(left);
}
ASTNodeType::Function { args, .. } => stack.extend(args.iter().rev()),
ASTNodeType::Call { callee, args } => {
stack.extend(args.iter().rev());
stack.push(callee);
}
ASTNodeType::Array(rows) => {
for row in rows.iter().rev() {
stack.extend(row.iter().rev());
}
}
ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
}
}
}
pub fn refs(&self) -> RefIter<'_> {
RefIter {
stack: smallvec::smallvec![self],
}
}
pub fn visit_refs<V: FnMut(RefView<'_>)>(&self, mut visitor: V) {
let mut stack: Vec<&ASTNode> = Vec::with_capacity(8);
stack.push(self);
while let Some(node) = stack.pop() {
match &node.node_type {
ASTNodeType::Reference { reference, .. } => visitor(RefView::from(reference)),
ASTNodeType::UnaryOp { expr, .. } => stack.push(expr),
ASTNodeType::BinaryOp { left, right, .. } => {
stack.push(right);
stack.push(left);
}
ASTNodeType::Function { args, .. } => {
for a in args.iter().rev() {
stack.push(a);
}
}
ASTNodeType::Call { callee, args } => {
for a in args.iter().rev() {
stack.push(a);
}
stack.push(callee);
}
ASTNodeType::Array(rows) => {
for r in rows.iter().rev() {
for item in r.iter().rev() {
stack.push(item);
}
}
}
ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
}
}
}
pub fn collect_references(&self, policy: &CollectPolicy) -> SmallVec<[ReferenceType; 4]> {
let mut out: SmallVec<[ReferenceType; 4]> = SmallVec::new();
self.visit_refs(|rv| match rv {
RefView::Cell {
sheet,
row,
col,
row_abs,
col_abs,
} => out.push(ReferenceType::Cell {
sheet: sheet.map(|s| s.to_string()),
row,
col,
row_abs,
col_abs,
}),
RefView::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => {
if policy.expand_small_ranges
&& let (Some(sr), Some(sc), Some(er), Some(ec)) =
(start_row, start_col, end_row, end_col)
{
let rows = u64::from(er.saturating_sub(sr)) + 1;
let cols = u64::from(ec.saturating_sub(sc)) + 1;
let area = rows.saturating_mul(cols);
let limit = u64::try_from(policy.range_expansion_limit).unwrap_or(u64::MAX);
if area <= limit {
let row_abs = start_row_abs && end_row_abs;
let col_abs = start_col_abs && end_col_abs;
for r in sr..=er {
for c in sc..=ec {
out.push(ReferenceType::Cell {
sheet: sheet.map(|s| s.to_string()),
row: r,
col: c,
row_abs,
col_abs,
});
}
}
return; }
}
out.push(ReferenceType::Range {
sheet: sheet.map(|s| s.to_string()),
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
});
}
RefView::Cell3D {
sheet_first,
sheet_last,
row,
col,
row_abs,
col_abs,
} => out.push(ReferenceType::Cell3D {
sheet_first: sheet_first.to_string(),
sheet_last: sheet_last.to_string(),
row,
col,
row_abs,
col_abs,
}),
RefView::Range3D {
sheet_first,
sheet_last,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => out.push(ReferenceType::Range3D {
sheet_first: sheet_first.to_string(),
sheet_last: sheet_last.to_string(),
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
}),
RefView::External {
raw,
book,
sheet,
kind,
} => out.push(ReferenceType::External(ExternalReference {
raw: raw.to_string(),
book: ExternalBookRef::Token(book.to_string()),
sheet: sheet.to_string(),
kind,
})),
RefView::Table { name, specifier } => out.push(ReferenceType::Table(TableReference {
name: name.to_string(),
specifier: specifier.cloned(),
})),
RefView::NamedRange { name } => {
if policy.include_names {
out.push(ReferenceType::NamedRange(name.to_string()));
}
}
});
out
}
pub fn update_sheet_references(&mut self, target_name: Option<&str>, new_name: &str) {
let mut stack: Vec<&mut ASTNode> = vec![self];
while let Some(node) = stack.pop() {
match &mut node.node_type {
ASTNodeType::Reference {
reference:
ReferenceType::Cell { sheet, .. } | ReferenceType::Range { sheet, .. },
..
} => {
if let Some(current_sheet) = sheet
&& (target_name.is_none() || target_name == Some(current_sheet.as_str()))
{
*sheet = Some(new_name.to_string());
}
}
ASTNodeType::Reference {
reference:
ReferenceType::Cell3D {
sheet_first,
sheet_last,
..
}
| ReferenceType::Range3D {
sheet_first,
sheet_last,
..
},
..
} => {
if target_name.is_none() || target_name == Some(sheet_first.as_str()) {
*sheet_first = new_name.to_string();
}
if target_name.is_none() || target_name == Some(sheet_last.as_str()) {
*sheet_last = new_name.to_string();
}
}
ASTNodeType::UnaryOp { expr, .. } => stack.push(expr),
ASTNodeType::BinaryOp { left, right, .. } => {
stack.push(right);
stack.push(left);
}
ASTNodeType::Function { args, .. } => stack.extend(args.iter_mut().rev()),
ASTNodeType::Call { callee, args } => {
stack.extend(args.iter_mut().rev());
stack.push(callee);
}
ASTNodeType::Array(rows) => {
for row in rows.iter_mut().rev() {
stack.extend(row.iter_mut().rev());
}
}
ASTNodeType::Reference { .. } | ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum RefView<'a> {
Cell {
sheet: Option<&'a str>,
row: u32,
col: u32,
row_abs: bool,
col_abs: bool,
},
Range {
sheet: Option<&'a str>,
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
},
Cell3D {
sheet_first: &'a str,
sheet_last: &'a str,
row: u32,
col: u32,
row_abs: bool,
col_abs: bool,
},
Range3D {
sheet_first: &'a str,
sheet_last: &'a str,
start_row: Option<u32>,
start_col: Option<u32>,
end_row: Option<u32>,
end_col: Option<u32>,
start_row_abs: bool,
start_col_abs: bool,
end_row_abs: bool,
end_col_abs: bool,
},
External {
raw: &'a str,
book: &'a str,
sheet: &'a str,
kind: ExternalRefKind,
},
Table {
name: &'a str,
specifier: Option<&'a TableSpecifier>,
},
NamedRange {
name: &'a str,
},
}
impl<'a> From<&'a ReferenceType> for RefView<'a> {
fn from(r: &'a ReferenceType) -> Self {
match r {
ReferenceType::Cell {
sheet,
row,
col,
row_abs,
col_abs,
} => RefView::Cell {
sheet: sheet.as_deref(),
row: *row,
col: *col,
row_abs: *row_abs,
col_abs: *col_abs,
},
ReferenceType::Range {
sheet,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => RefView::Range {
sheet: sheet.as_deref(),
start_row: *start_row,
start_col: *start_col,
end_row: *end_row,
end_col: *end_col,
start_row_abs: *start_row_abs,
start_col_abs: *start_col_abs,
end_row_abs: *end_row_abs,
end_col_abs: *end_col_abs,
},
ReferenceType::Cell3D {
sheet_first,
sheet_last,
row,
col,
row_abs,
col_abs,
} => RefView::Cell3D {
sheet_first: sheet_first.as_str(),
sheet_last: sheet_last.as_str(),
row: *row,
col: *col,
row_abs: *row_abs,
col_abs: *col_abs,
},
ReferenceType::Range3D {
sheet_first,
sheet_last,
start_row,
start_col,
end_row,
end_col,
start_row_abs,
start_col_abs,
end_row_abs,
end_col_abs,
} => RefView::Range3D {
sheet_first: sheet_first.as_str(),
sheet_last: sheet_last.as_str(),
start_row: *start_row,
start_col: *start_col,
end_row: *end_row,
end_col: *end_col,
start_row_abs: *start_row_abs,
start_col_abs: *start_col_abs,
end_row_abs: *end_row_abs,
end_col_abs: *end_col_abs,
},
ReferenceType::External(ext) => RefView::External {
raw: ext.raw.as_str(),
book: ext.book.token(),
sheet: ext.sheet.as_str(),
kind: ext.kind,
},
ReferenceType::Table(tr) => RefView::Table {
name: tr.name.as_str(),
specifier: tr.specifier.as_ref(),
},
ReferenceType::NamedRange(name) => RefView::NamedRange { name },
}
}
}
pub struct RefIter<'a> {
stack: smallvec::SmallVec<[&'a ASTNode; 8]>,
}
impl<'a> Iterator for RefIter<'a> {
type Item = RefView<'a>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(node) = self.stack.pop() {
match &node.node_type {
ASTNodeType::Reference { reference, .. } => return Some(RefView::from(reference)),
ASTNodeType::UnaryOp { expr, .. } => self.stack.push(expr),
ASTNodeType::BinaryOp { left, right, .. } => {
self.stack.push(right);
self.stack.push(left);
}
ASTNodeType::Function { args, .. } => {
for a in args.iter().rev() {
self.stack.push(a);
}
}
ASTNodeType::Call { callee, args } => {
for a in args.iter().rev() {
self.stack.push(a);
}
self.stack.push(callee);
}
ASTNodeType::Array(rows) => {
for r in rows.iter().rev() {
for item in r.iter().rev() {
self.stack.push(item);
}
}
}
ASTNodeType::Literal(_) | ASTNodeType::Omitted => {}
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct CollectPolicy {
pub expand_small_ranges: bool,
pub range_expansion_limit: usize,
pub include_names: bool,
}
impl Default for CollectPolicy {
fn default() -> Self {
Self {
expand_small_ranges: false,
range_expansion_limit: 0,
include_names: true,
}
}
}
impl Display for ASTNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.node_type)
}
}
impl std::hash::Hash for ASTNode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let hash = self.calculate_hash();
state.write_u64(hash);
}
}
impl From<TokenizerError> for ParserError {
fn from(err: TokenizerError) -> Self {
ParserError {
message: err.message,
position: Some(err.pos),
}
}
}
pub struct Parser {
source: Arc<str>,
tokens: Arc<[TokenSpan]>,
position: usize,
volatility_classifier: Option<VolatilityClassifierBox>,
dialect: FormulaDialect,
in_call_args_depth: usize,
depth: usize,
limits: ParserLimits,
nodes: usize,
pending_error: Option<ParserError>,
}
struct BuiltNode {
node: ASTNode,
height: usize,
}
#[derive(Default)]
struct BuiltArgs {
nodes: Vec<ASTNode>,
height: usize,
volatile: bool,
}
impl BuiltArgs {
fn push(&mut self, child: BuiltNode) {
self.height = self.height.max(child.height);
self.volatile |= child.node.contains_volatile;
self.nodes.push(child.node);
}
}
impl std::ops::Deref for BuiltNode {
type Target = ASTNode;
fn deref(&self) -> &ASTNode {
&self.node
}
}
impl Parser {
pub fn new<T: AsRef<str>>(formula: T) -> Result<Self, TokenizerError> {
Self::new_with_dialect(formula, FormulaDialect::Excel)
}
pub fn try_from_formula(formula: &str) -> Result<Self, TokenizerError> {
Self::new(formula)
}
pub fn new_with_dialect<T: AsRef<str>>(
formula: T,
dialect: FormulaDialect,
) -> Result<Self, TokenizerError> {
Self::new_with_limits(formula.as_ref(), dialect, ParserLimits::default())
}
fn new_with_limits(
formula: &str,
dialect: FormulaDialect,
limits: ParserLimits,
) -> Result<Self, TokenizerError> {
limits.check_source(formula)?;
let spans = crate::tokenizer::tokenize_spans_with_limits(formula, dialect, limits)?;
let mut parser = Self::from_source_and_tokens(
Arc::from(formula),
Arc::from(spans.into_boxed_slice()),
dialect,
);
parser.limits = limits;
Ok(parser)
}
pub fn from_token_stream(stream: &TokenStream) -> Self {
let limits = ParserLimits::default();
let error = stream.admission_error(limits).map(ParserError::from);
if let Some(error) = error {
let mut parser =
Self::from_source_and_tokens(Arc::from(""), Arc::from([]), stream.dialect());
parser.pending_error = Some(error);
return parser;
}
Self::from_source_and_tokens(
Arc::from(stream.source()),
Arc::from(stream.spans.clone().into_boxed_slice()),
stream.dialect(),
)
}
fn from_source_and_tokens(
source: Arc<str>,
tokens: Arc<[TokenSpan]>,
dialect: FormulaDialect,
) -> Self {
Parser {
source,
tokens,
position: 0,
volatility_classifier: None,
dialect,
in_call_args_depth: 0,
depth: 0,
limits: ParserLimits::default(),
nodes: 0,
pending_error: None,
}
}
pub fn with_volatility_classifier<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.volatility_classifier = Some(Box::new(f));
self
}
#[expect(
clippy::arithmetic_side_effects,
reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
)]
fn unary(&mut self, expr: BuiltNode, span: TokenSpan) -> Result<BuiltNode, ParserError> {
let height = 1 + expr.height;
self.admit(height)?;
let token = self.span_to_token(&span);
let volatile = expr.contains_volatile;
Ok(BuiltNode {
height,
node: ASTNode::new_with_volatile(
ASTNodeType::UnaryOp {
op: UnaryOperator::try_from(token.value.as_str()).map_err(|message| {
ParserError {
message: message.to_string(),
position: Some(span.start),
}
})?,
expr: Box::new(expr.node),
},
Some(token),
volatile,
),
})
}
#[expect(
clippy::arithmetic_side_effects,
reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
)]
fn binary(
&mut self,
left: BuiltNode,
right: BuiltNode,
span: TokenSpan,
) -> Result<BuiltNode, ParserError> {
let height = 1 + left.height.max(right.height);
self.admit(height)?;
let token = self.span_to_token(&span);
let volatile = left.contains_volatile || right.contains_volatile;
Ok(BuiltNode {
height,
node: ASTNode::new_with_volatile(
ASTNodeType::BinaryOp {
op: BinaryOperator::try_from(token.value.as_str()).map_err(|message| {
ParserError {
message: if token.value == ";" {
"';' is an argument separator, not an operator in this position; OpenFormula union uses '~'".to_string()
} else {
message.to_string()
},
position: Some(span.start),
}
})?,
left: Box::new(left.node),
right: Box::new(right.node),
},
Some(token),
volatile,
),
})
}
#[expect(
clippy::arithmetic_side_effects,
reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
)]
fn call(&mut self, left: BuiltNode, args: BuiltArgs) -> Result<BuiltNode, ParserError> {
let height = 1 + left.height.max(args.height);
self.admit(height)?;
let volatile = left.contains_volatile || args.volatile;
Ok(BuiltNode {
height,
node: ASTNode::new_with_volatile(
ASTNodeType::Call {
callee: Box::new(left.node),
args: args.nodes,
},
None,
volatile,
),
})
}
#[inline]
fn peek(&self) -> Option<TokenSpan> {
self.tokens.get(self.position).copied()
}
#[inline]
fn peek_is(&self, token_type: TokenType, subtype: TokenSubType) -> bool {
self.peek()
.is_some_and(|t| t.token_type == token_type && t.subtype == subtype)
}
#[inline]
#[expect(
clippy::arithmetic_side_effects,
reason = "callers advance only past a token they read with peek, so position < tokens.len()"
)]
fn advance(&mut self) {
self.position += 1;
}
fn skip_whitespace(&mut self) {
while self
.peek()
.is_some_and(|t| t.token_type == TokenType::Whitespace)
{
self.advance();
}
}
#[expect(
clippy::string_slice,
reason = "spans come from the span tokenizer on this source (char-boundary invariant), or from an external TokenStream whose spans admission_error checked with source.get"
)]
fn span_value(&self, span: &TokenSpan) -> &str {
&self.source[span.start..span.end]
}
fn semantic_span_value(&self, span: &TokenSpan) -> &str {
let value = self.span_value(span);
if span.token_type == TokenType::OpInfix
&& value.as_bytes().contains(&b' ')
&& value
.as_bytes()
.iter()
.all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
{
" "
} else {
value
}
}
fn span_to_token(&self, span: &TokenSpan) -> Token {
Token::new_with_span(
self.semantic_span_value(span).to_string(),
span.token_type,
span.subtype,
span.start,
span.end,
)
}
fn span_precedence(&self, span: &TokenSpan) -> Option<(u8, Associativity)> {
if !matches!(
span.token_type,
TokenType::OpPrefix | TokenType::OpInfix | TokenType::OpPostfix
) {
return None;
}
let op = if span.token_type == TokenType::OpPrefix {
"u"
} else {
self.semantic_span_value(span)
};
match op {
"#" => Some((11, Associativity::Left)),
":" => Some((10, Associativity::Left)),
" " => Some((9, Associativity::Left)),
"," => Some((8, Associativity::Left)),
"%" => Some((7, Associativity::Left)),
"u" => Some((6, Associativity::Right)),
"^" => Some((5, Associativity::Left)),
"*" | "/" => Some((4, Associativity::Left)),
"+" | "-" => Some((3, Associativity::Left)),
"&" => Some((2, Associativity::Left)),
"=" | "<" | ">" | "<=" | ">=" | "<>" => Some((1, Associativity::Left)),
_ => None,
}
}
#[cold]
fn limit_error(&self, budget: &str, limit: usize) -> ParserError {
ParserError {
message: format!("Formula {budget} (max {limit})"),
position: Some(self.position),
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "nodes < ast_nodes is checked before the increment"
)]
fn admit(&mut self, height: usize) -> Result<(), ParserError> {
if self.nodes >= self.limits.ast_nodes {
return Err(self.limit_error("AST node limit exceeded", self.limits.ast_nodes));
}
if height > self.limits.ast_height {
return Err(self.limit_error("AST height limit exceeded", self.limits.ast_height));
}
self.nodes += 1;
Ok(())
}
fn omitted(&mut self) -> Result<BuiltNode, ParserError> {
self.admit(1)?;
Ok(BuiltNode {
node: ASTNode::new(ASTNodeType::Omitted, None),
height: 1,
})
}
pub fn parse(&mut self) -> Result<ASTNode, ParserError> {
if let Some(error) = &self.pending_error {
return Err(ParserError {
message: error.message.clone(),
position: error.position,
});
}
self.nodes = 0;
if self.tokens.is_empty() {
return Err(ParserError {
message: "No tokens to parse".to_string(),
position: None,
});
}
self.skip_whitespace();
let Some(first) = self.peek() else {
return Err(ParserError {
message: "No tokens to parse".to_string(),
position: None,
});
};
if first.token_type == TokenType::Literal {
let span = first;
self.advance();
self.skip_whitespace();
if let Some(extra) = self.peek() {
return Err(ParserError {
message: format!("Unexpected token at position {}: {extra:?}", self.position),
position: Some(self.position),
});
}
let token = self.span_to_token(&span);
self.admit(1)?;
return Ok(ASTNode::new(
ASTNodeType::Literal(LiteralValue::Text(token.value.clone())),
Some(token),
));
}
let ast = self.parse_expression()?;
self.skip_whitespace();
if let Some(extra) = self.peek() {
return Err(ParserError {
message: format!("Unexpected token at position {}: {extra:?}", self.position),
position: Some(self.position),
});
}
Ok(ast.node)
}
fn parse_expression(&mut self) -> Result<BuiltNode, ParserError> {
self.parse_bp(0)
}
#[expect(
clippy::arithmetic_side_effects,
reason = "depth counts active parse_bp frames: it is incremented once per frame and decremented on every exit, so it is bounded by the call stack"
)]
fn parse_bp(&mut self, min_precedence: u8) -> Result<BuiltNode, ParserError> {
self.depth += 1;
if self.depth > self.limits.pratt_frames {
self.depth -= 1;
return Err(self.limit_error("nesting too deep", self.limits.pratt_frames));
}
let result = self.parse_bp_inner(min_precedence);
self.depth -= 1;
result
}
#[expect(
clippy::arithmetic_side_effects,
reason = "precedence comes from the span_precedence table (at most 11), so precedence + 1 fits in u8"
)]
fn parse_bp_inner(&mut self, min_precedence: u8) -> Result<BuiltNode, ParserError> {
let mut left = self.parse_prefix()?;
loop {
self.skip_whitespace();
let Some(token) = self.peek() else {
break;
};
if token.token_type == TokenType::Paren && token.subtype == TokenSubType::Open {
self.advance();
let args = self.parse_call_arguments()?;
left = self.call(left, args)?;
continue;
}
if token.token_type == TokenType::OpPostfix {
let (precedence, _) = self
.span_precedence(&token)
.unwrap_or((0, Associativity::Left));
if precedence < min_precedence {
break;
}
self.advance();
left = self.unary(left, token)?;
continue;
}
if token.token_type != TokenType::OpInfix {
break;
}
if self.in_call_args_depth > 0 && self.span_value(&token) == "," {
break;
}
let (precedence, associativity) = self
.span_precedence(&token)
.unwrap_or((0, Associativity::Left));
if precedence < min_precedence {
break;
}
let op_span = token;
self.advance();
let next_min_precedence = if associativity == Associativity::Left {
precedence + 1
} else {
precedence
};
let right = self.parse_bp(next_min_precedence)?;
left = self.binary(left, right, op_span)?;
}
Ok(left)
}
fn parse_prefix(&mut self) -> Result<BuiltNode, ParserError> {
self.skip_whitespace();
if let Some(op_span) = self.peek()
&& op_span.token_type == TokenType::OpPrefix
{
self.advance();
let (precedence, _) = self
.span_precedence(&op_span)
.unwrap_or((0, Associativity::Right));
let expr = self.parse_bp(precedence)?;
return self.unary(expr, op_span);
}
self.parse_primary()
}
fn parse_primary(&mut self) -> Result<BuiltNode, ParserError> {
self.skip_whitespace();
let Some(token) = self.peek() else {
return Err(ParserError {
message: "Unexpected end of tokens".to_string(),
position: Some(self.position),
});
};
match token.token_type {
TokenType::Operand => {
self.advance();
self.parse_operand(token)
}
TokenType::Func => {
self.advance();
self.parse_function(token)
}
TokenType::Paren if token.subtype == TokenSubType::Open => {
self.advance();
let expr = self.parse_expression()?;
self.skip_whitespace();
if !self.peek_is(TokenType::Paren, TokenSubType::Close) {
return Err(ParserError {
message: "Expected closing parenthesis".to_string(),
position: Some(self.position),
});
}
self.advance();
Ok(expr)
}
TokenType::Array if token.subtype == TokenSubType::Open => {
self.advance();
self.parse_array()
}
_ => Err(ParserError {
message: format!("Unexpected token: {token:?}"),
position: Some(self.position),
}),
}
}
fn parse_operand(&mut self, span: TokenSpan) -> Result<BuiltNode, ParserError> {
self.admit(1)?;
let value = self.span_value(&span);
let token = self.span_to_token(&span);
match span.subtype {
TokenSubType::Number => {
let value = value.parse::<f64>().map_err(|_| ParserError {
message: format!("Invalid number: {value}"),
position: Some(self.position),
})?;
Ok(BuiltNode {
height: 1,
node: ASTNode::new(
ASTNodeType::Literal(LiteralValue::Number(value)),
Some(token),
),
})
}
TokenSubType::Text => {
let text = match value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
Some(inner) => inner.replace("\"\"", "\""),
None => value.to_string(),
};
Ok(BuiltNode {
height: 1,
node: ASTNode::new(ASTNodeType::Literal(LiteralValue::Text(text)), Some(token)),
})
}
TokenSubType::Logical => {
let v = value.eq_ignore_ascii_case("TRUE");
Ok(BuiltNode {
height: 1,
node: ASTNode::new(ASTNodeType::Literal(LiteralValue::Boolean(v)), Some(token)),
})
}
TokenSubType::Error => {
let error = ExcelError::from_error_string(value);
Ok(BuiltNode {
height: 1,
node: ASTNode::new(
ASTNodeType::Literal(LiteralValue::Error(error)),
Some(token),
),
})
}
TokenSubType::Range => {
let reference = ReferenceType::from_string_with_dialect(value, self.dialect)
.map_err(|e| ParserError {
message: format!("Invalid reference '{value}': {e}"),
position: Some(self.position),
})?;
Ok(BuiltNode {
height: 1,
node: ASTNode::new(
ASTNodeType::Reference {
original: value.to_string(),
reference,
},
Some(token),
),
})
}
_ => Err(ParserError {
message: format!("Unexpected operand subtype: {:?}", span.subtype),
position: Some(self.position),
}),
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
)]
fn parse_function(&mut self, func_span: TokenSpan) -> Result<BuiltNode, ParserError> {
let Some(name) = self.span_value(&func_span).strip_suffix('(') else {
return Err(ParserError {
message: "Invalid function token".to_string(),
position: Some(self.position),
});
};
let name = name.to_string();
let args = self.parse_function_arguments()?;
let this_is_volatile = self
.volatility_classifier
.as_ref()
.map(|f| f(name.as_str()))
.unwrap_or(false);
let args_volatile = args.volatile;
let height = 1 + args.height;
self.admit(height)?;
let func_token = self.span_to_token(&func_span);
Ok(BuiltNode {
height,
node: ASTNode::new_with_volatile(
ASTNodeType::Function {
name,
args: args.nodes,
},
Some(func_token),
this_is_volatile || args_volatile,
),
})
}
#[expect(
clippy::arithmetic_side_effects,
reason = "in_call_args_depth is incremented and decremented once per call, so it counts active calls and is bounded by the Pratt-frame limit"
)]
fn parse_call_arguments(&mut self) -> Result<BuiltArgs, ParserError> {
let mut args = BuiltArgs::default();
self.skip_whitespace();
if self.peek_is(TokenType::Paren, TokenSubType::Close) {
self.advance();
return Ok(args);
}
self.in_call_args_depth += 1;
let result = (|| -> Result<BuiltArgs, ParserError> {
let mut expecting_argument = true;
let mut saw_argument = false;
loop {
self.skip_whitespace();
let Some(token) = self.peek() else {
return Err(ParserError {
message: "Unterminated call argument list".to_string(),
position: Some(self.position),
});
};
let is_separator = (token.token_type == TokenType::Sep
&& token.subtype == TokenSubType::Arg)
|| (token.token_type == TokenType::OpInfix && self.span_value(&token) == ",");
let is_close =
token.token_type == TokenType::Paren && token.subtype == TokenSubType::Close;
if expecting_argument {
if is_close {
if saw_argument {
args.push(self.omitted()?);
}
self.advance();
return Ok(std::mem::take(&mut args));
}
if is_separator {
args.push(self.omitted()?);
saw_argument = true;
self.advance();
} else {
args.push(self.parse_expression()?);
saw_argument = true;
expecting_argument = false;
}
} else if is_separator {
self.advance();
expecting_argument = true;
} else if is_close {
self.advance();
return Ok(std::mem::take(&mut args));
} else {
return Err(ParserError {
message: format!("Expected ',' or ')' in call arguments, got {token:?}"),
position: Some(self.position),
});
}
}
})();
self.in_call_args_depth -= 1;
result
}
fn parse_function_arguments(&mut self) -> Result<BuiltArgs, ParserError> {
let mut args = BuiltArgs::default();
self.skip_whitespace();
if self.peek_is(TokenType::Func, TokenSubType::Close) {
self.advance();
return Ok(args);
}
let mut expecting_argument = true;
let mut saw_argument = false;
loop {
self.skip_whitespace();
let Some(token) = self.peek() else {
return Err(ParserError {
message: "Unterminated function argument list".to_string(),
position: Some(self.position),
});
};
let is_separator =
token.token_type == TokenType::Sep && token.subtype == TokenSubType::Arg;
let is_close =
token.token_type == TokenType::Func && token.subtype == TokenSubType::Close;
if expecting_argument {
if is_close {
if saw_argument {
args.push(self.omitted()?);
}
self.advance();
return Ok(args);
}
if is_separator {
args.push(self.omitted()?);
saw_argument = true;
self.advance();
} else {
args.push(self.parse_expression()?);
saw_argument = true;
expecting_argument = false;
}
} else if is_separator {
self.advance();
expecting_argument = true;
} else if is_close {
self.advance();
return Ok(args);
} else {
return Err(ParserError {
message: format!("Expected ',' or ')' in function arguments, got {token:?}"),
position: Some(self.position),
});
}
}
}
#[expect(
clippy::arithmetic_side_effects,
reason = "child heights are at most the AST height limit and the number of admitted nodes, so 1 + height cannot overflow"
)]
fn append_array_item(
&mut self,
row: &mut Vec<ASTNode>,
height: &mut usize,
volatile: &mut bool,
) -> Result<(), ParserError> {
let child = self.parse_expression()?;
*height = (*height).max(1 + child.height);
*volatile |= child.contains_volatile;
row.push(child.node);
Ok(())
}
fn finish_array(
&mut self,
rows: Vec<Vec<ASTNode>>,
height: usize,
volatile: bool,
) -> Result<BuiltNode, ParserError> {
self.admit(height)?;
Ok(BuiltNode {
height,
node: ASTNode::new_with_volatile(ASTNodeType::Array(rows), None, volatile),
})
}
fn parse_array(&mut self) -> Result<BuiltNode, ParserError> {
let mut rows = Vec::new();
let mut current_row = Vec::new();
let mut height = 1;
let mut contains_volatile = false;
self.skip_whitespace();
if self.peek_is(TokenType::Array, TokenSubType::Close) {
self.advance();
return self.finish_array(rows, 1, false);
}
self.append_array_item(&mut current_row, &mut height, &mut contains_volatile)?;
while self.position < self.tokens.len() {
self.skip_whitespace();
let Some(token) = self.peek() else {
break;
};
match (token.token_type, token.subtype) {
(TokenType::Sep, TokenSubType::Arg) => {
self.advance();
self.append_array_item(&mut current_row, &mut height, &mut contains_volatile)?;
}
(TokenType::Sep, TokenSubType::Row) => {
self.advance();
rows.push(current_row);
current_row = Vec::new();
self.append_array_item(&mut current_row, &mut height, &mut contains_volatile)?;
}
(TokenType::Array, TokenSubType::Close) => {
self.advance();
rows.push(current_row);
break;
}
_ => {
return Err(ParserError {
message: format!("Unexpected token in array: {token:?}"),
position: Some(self.position),
});
}
}
}
if let Some(first) = rows.first()
&& rows.iter().any(|row| row.len() != first.len())
{
return Err(ParserError {
message: "Array rows must have equal length".to_string(),
position: Some(self.position.saturating_sub(1)),
});
}
self.finish_array(rows, height, contains_volatile)
}
}
impl TryFrom<&str> for Parser {
type Error = TokenizerError;
fn try_from(formula: &str) -> Result<Self, Self::Error> {
Self::new(formula)
}
}
impl TryFrom<String> for Parser {
type Error = TokenizerError;
fn try_from(formula: String) -> Result<Self, Self::Error> {
Self::new(formula)
}
}
impl From<&TokenStream> for Parser {
fn from(stream: &TokenStream) -> Self {
Self::from_token_stream(stream)
}
}
impl FromStr for ASTNode {
type Err = ParserError;
fn from_str(formula: &str) -> Result<Self, Self::Err> {
parse(formula)
}
}
impl TryFrom<&str> for ASTNode {
type Error = ParserError;
fn try_from(formula: &str) -> Result<Self, Self::Error> {
parse(formula)
}
}
impl TryFrom<String> for ASTNode {
type Error = ParserError;
fn try_from(formula: String) -> Result<Self, Self::Error> {
parse(formula)
}
}
impl Parser {
pub fn builder() -> ParserBuilder {
ParserBuilder::default()
}
}
#[derive(Default)]
pub struct ParserBuilder {
dialect: FormulaDialect,
volatility_classifier: Option<VolatilityClassifierArc>,
limits: ParserLimits,
}
impl ParserBuilder {
pub fn limits(mut self, limits: ParserLimits) -> Self {
self.limits = limits;
self
}
pub fn dialect(mut self, dialect: FormulaDialect) -> Self {
self.dialect = dialect;
self
}
pub fn with_volatility_classifier<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.volatility_classifier = Some(Arc::new(f));
self
}
pub fn build<T: AsRef<str>>(self, formula: T) -> Result<Parser, TokenizerError> {
let mut parser = Parser::new_with_limits(formula.as_ref(), self.dialect, self.limits)?;
if let Some(classifier) = self.volatility_classifier {
parser = parser.with_volatility_classifier(move |name| classifier(name));
}
Ok(parser)
}
pub fn parse<T: AsRef<str>>(self, formula: T) -> Result<ASTNode, ParserError> {
let mut parser = self.build(formula)?;
parser.parse()
}
}
pub fn normalise_reference(reference: &str) -> Result<String, ParsingError> {
let ref_type = ReferenceType::from_string(reference)?;
Ok(ref_type.to_string())
}
pub fn parse<T: AsRef<str>>(formula: T) -> Result<ASTNode, ParserError> {
parse_with_dialect(formula, FormulaDialect::Excel)
}
pub fn parse_with_dialect<T: AsRef<str>>(
formula: T,
dialect: FormulaDialect,
) -> Result<ASTNode, ParserError> {
let mut parser = Parser::new_with_dialect(formula, dialect)?;
parser.parse()
}
pub fn parse_with_volatility_classifier<T, F>(
formula: T,
classifier: F,
) -> Result<ASTNode, ParserError>
where
T: AsRef<str>,
F: Fn(&str) -> bool + Send + Sync + 'static,
{
parse_with_dialect_and_volatility_classifier(formula, FormulaDialect::Excel, classifier)
}
pub fn parse_with_dialect_and_volatility_classifier<T, F>(
formula: T,
dialect: FormulaDialect,
classifier: F,
) -> Result<ASTNode, ParserError>
where
T: AsRef<str>,
F: Fn(&str) -> bool + Send + Sync + 'static,
{
let mut parser =
Parser::new_with_dialect(formula, dialect)?.with_volatility_classifier(classifier);
parser.parse()
}
pub struct BatchParser {
include_whitespace: bool,
volatility_classifier: Option<VolatilityClassifierArc>,
token_cache: crate::token_cache::TokenCache,
limits: ParserLimits,
dialect: FormulaDialect,
}
impl BatchParser {
pub fn builder() -> BatchParserBuilder {
BatchParserBuilder::default()
}
pub fn parse(&mut self, formula: &str) -> Result<ASTNode, ParserError> {
self.limits.check_source(formula)?;
let (source, spans) = if let Some(pair) = self.token_cache.get(formula) {
pair
} else {
let mut spans =
crate::tokenizer::tokenize_spans_with_limits(formula, self.dialect, self.limits)?;
let source: Arc<str> = Arc::from(formula);
if !self.include_whitespace {
spans.retain(|t| t.token_type != TokenType::Whitespace);
}
let spans: Arc<[TokenSpan]> = Arc::from(spans.into_boxed_slice());
self.token_cache
.insert(Arc::clone(&source), Arc::clone(&spans));
(source, spans)
};
let mut parser = Parser::from_source_and_tokens(source, spans, self.dialect);
parser.limits = self.limits;
if let Some(classifier) = self.volatility_classifier.clone() {
parser = parser.with_volatility_classifier(move |name| classifier(name));
}
parser.parse()
}
}
pub struct BatchParserBuilder {
limits: ParserLimits,
cache_entries: usize,
cache_bytes: usize,
include_whitespace: bool,
volatility_classifier: Option<VolatilityClassifierArc>,
dialect: FormulaDialect,
}
impl Default for BatchParserBuilder {
fn default() -> Self {
Self {
include_whitespace: false,
volatility_classifier: None,
dialect: FormulaDialect::default(),
limits: ParserLimits::default(),
cache_entries: 16_384,
cache_bytes: 8 * 1024 * 1024,
}
}
}
impl BatchParserBuilder {
pub fn limits(mut self, limits: ParserLimits) -> Self {
self.limits = limits;
self
}
pub fn cache_capacity(mut self, entries: usize, bytes: usize) -> Self {
self.cache_entries = entries;
self.cache_bytes = bytes;
self
}
pub fn include_whitespace(mut self, include: bool) -> Self {
self.include_whitespace = include;
self
}
pub fn with_volatility_classifier<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.volatility_classifier = Some(Arc::new(f));
self
}
pub fn dialect(mut self, dialect: FormulaDialect) -> Self {
self.dialect = dialect;
self
}
pub fn build(self) -> BatchParser {
BatchParser {
include_whitespace: self.include_whitespace,
volatility_classifier: self.volatility_classifier,
token_cache: crate::token_cache::TokenCache::new(self.cache_entries, self.cache_bytes),
limits: self.limits,
dialect: self.dialect,
}
}
}