#![allow(clippy::result_large_err)]
use std::collections::HashSet;
use crate::ast::{
BinaryOp, Direction, ExprKind, Expression, IndexType, InequalityOp, IntegralBounds, LogicalOp,
MathConstant, MathFloat, RelationOp, SetOp, SetRelation, TensorIndex, VectorNotation,
};
use crate::error::{ParseError, ParseOutput, ParseResult, Span};
use crate::parser::latex_tokenizer::{tokenize_latex, LatexToken};
use crate::parser::Spanned;
mod arithmetic;
mod calculus;
mod commands;
mod derivatives;
mod expression;
mod linear_algebra;
mod primary;
pub fn parse_latex(input: &str) -> ParseResult<Expression> {
let tokens = tokenize_latex(input)?;
let parser = LatexParser::new(tokens, false);
parser.parse_strict()
}
pub fn parse_latex_equation_system(input: &str) -> ParseResult<Vec<Expression>> {
input
.split(';')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(parse_latex)
.collect()
}
pub fn parse_latex_lenient(input: &str) -> ParseOutput {
let tokens = match tokenize_latex(input) {
Ok(tokens) => tokens,
Err(err) => {
return ParseOutput {
expression: None,
errors: vec![err],
}
}
};
let parser = LatexParser::new(tokens, true);
parser.parse_lenient()
}
struct LatexParser {
tokens: Vec<Spanned<LatexToken>>,
pos: usize,
bound_scopes: Vec<HashSet<String>>,
in_integral_context: bool,
in_fraction_context: bool,
collected_errors: Vec<ParseError>,
}
impl LatexParser {
fn new(tokens: Vec<Spanned<LatexToken>>, _lenient: bool) -> Self {
Self {
tokens,
pos: 0,
bound_scopes: Vec::new(),
in_integral_context: false,
in_fraction_context: false,
collected_errors: Vec::new(),
}
}
fn push_scope(&mut self, vars: impl IntoIterator<Item = String>) {
self.bound_scopes.push(vars.into_iter().collect());
}
fn pop_scope(&mut self) {
self.bound_scopes.pop();
}
fn is_bound(&self, name: &str) -> bool {
self.bound_scopes.iter().any(|scope| scope.contains(name))
}
fn resolve_letter(&self, ch: char, is_explicit: bool) -> Expression {
let name = ch.to_string();
if self.is_bound(&name) {
return ExprKind::Variable(name).into();
}
if is_explicit {
return match ch {
'e' => ExprKind::Constant(MathConstant::E).into(),
'i' => ExprKind::Constant(MathConstant::I).into(),
'j' => ExprKind::Constant(MathConstant::J).into(),
'k' => ExprKind::Constant(MathConstant::K).into(),
_ => ExprKind::Variable(name).into(),
};
}
if ch == 'e' || ch == 'i' {
return match ch {
'e' => ExprKind::Constant(MathConstant::E).into(),
'i' => ExprKind::Constant(MathConstant::I).into(),
_ => unreachable!(),
};
}
ExprKind::Variable(name).into()
}
fn peek(&self) -> Option<&Spanned<LatexToken>> {
self.tokens.get(self.pos)
}
fn peek_ahead(&self, offset: usize) -> Option<&Spanned<LatexToken>> {
self.tokens.get(self.pos + offset)
}
fn next(&mut self) -> Option<Spanned<LatexToken>> {
let token = self.tokens.get(self.pos).cloned();
if token.is_some() {
self.pos += 1;
}
token
}
fn current_span(&self) -> Span {
self.peek().map(|(_, span)| *span).unwrap_or_else(|| {
if let Some((_, last_span)) = self.tokens.last() {
Span::at(last_span.end)
} else {
Span::start()
}
})
}
fn check(&self, expected: &LatexToken) -> bool {
self.peek().map(|(tok, _)| tok == expected).unwrap_or(false)
}
fn consume(&mut self, expected: LatexToken) -> ParseResult<Span> {
if let Some((token, span)) = self.next() {
if token == expected {
Ok(span)
} else {
Err(ParseError::unexpected_token(
vec![format!("{:?}", expected)],
format!("{:?}", token),
Some(span),
))
}
} else {
Err(ParseError::unexpected_eof(
vec![format!("{:?}", expected)],
Some(self.current_span()),
))
}
}
fn is_sync_token(token: &LatexToken) -> bool {
matches!(
token,
LatexToken::RBrace
| LatexToken::Plus
| LatexToken::Minus
| LatexToken::Equals
| LatexToken::Eof
) || matches!(token, LatexToken::Command(cmd) if matches!(
cmd.as_str(),
"frac" | "sqrt" | "sum" | "prod" | "int" | "iint" | "iiint" | "oint" | "lim"
))
}
fn synchronize(&mut self) {
while let Some((token, _)) = self.peek() {
if Self::is_sync_token(token) {
return;
}
self.next();
}
}
fn parse_strict(mut self) -> ParseResult<Expression> {
let expr = self.parse_expression()?;
if let Some((token, span)) = self.peek() {
if !matches!(token, LatexToken::Eof) {
return Err(ParseError::unexpected_token(
vec!["end of input"],
format!("{:?}", token),
Some(*span),
));
}
}
Ok(expr)
}
fn parse_lenient(mut self) -> ParseOutput {
let mut parts: Vec<Expression> = Vec::new();
while self.peek().is_some() && !matches!(self.peek(), Some((LatexToken::Eof, _))) {
match self.parse_expression() {
Ok(expr) => {
parts.push(expr);
if let Some((token, _)) = self.peek() {
if !matches!(token, LatexToken::Eof) {
let span = self.current_span();
self.collected_errors.push(ParseError::unexpected_token(
vec!["end of input or operator"],
format!("{:?}", token),
Some(span),
));
self.synchronize();
}
}
}
Err(err) => {
self.collected_errors.push(err);
self.synchronize();
if let Some((token, _)) = self.peek() {
if !matches!(token, LatexToken::Eof) && matches!(token, LatexToken::RBrace)
{
self.next();
}
}
}
}
}
let expression = match parts.len() {
0 => None,
1 => Some(parts.remove(0)),
_ => {
Some(parts.remove(0))
}
};
ParseOutput {
expression,
errors: self.collected_errors,
}
}
}
#[cfg(test)]
#[path = "latex/tests/latex_tests_greek.rs"]
mod greek_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_fractions.rs"]
mod fractions_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_roots.rs"]
#[allow(clippy::approx_constant)]
mod roots_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_powers_subscripts.rs"]
mod powers_subscripts_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_functions.rs"]
#[allow(clippy::approx_constant)]
mod functions_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_calculus.rs"]
mod calculus_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_constants.rs"]
mod constants_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_errors.rs"]
mod errors_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_tensors.rs"]
mod tensors_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_vectors.rs"]
mod vectors_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_multiple_integrals.rs"]
mod multiple_integrals_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_logic.rs"]
mod logic_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_sets.rs"]
mod sets_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_quaternions.rs"]
mod quaternions_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_differential_forms.rs"]
mod differential_forms_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_inline.rs"]
#[allow(clippy::approx_constant)]
mod tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_log_base.rs"]
mod log_base_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_floor_ceil.rs"]
mod floor_ceil_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_abs_sgn.rs"]
mod abs_sgn_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_det.rs"]
mod det_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_transpose.rs"]
mod transpose_tests;
#[cfg(test)]
#[path = "latex/tests/latex_tests_equation_system.rs"]
mod equation_system_tests;