machine-check-common 0.4.0

Utility crate for the formal verification tool machine-check
Documentation
use std::{collections::VecDeque, iter::Peekable};

use crate::{property::ComparisonType, ExecError};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bracket {
    Parenthesis,
    Square,
    Curly,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenType {
    Comma,
    ExclamationMark,
    LogicAnd,
    LogicOr,
    OpeningBracket(Bracket),
    ClosingBracket(Bracket),
    Ident(String),
    Number(u64),
    Comparison(ComparisonType),
    MacroInvocation(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TokenSpan {
    pub start: usize,
    pub end: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
    pub ty: TokenType,
    pub span: TokenSpan,
}

/// Lexes a property string to a sequence of tokens.
pub fn lex(input: &str) -> Result<VecDeque<Token>, ExecError> {
    fn add_token(tokens: &mut VecDeque<Token>, start: usize, end: usize, ty: TokenType) {
        tokens.push_back(Token {
            ty,
            span: TokenSpan { start, end },
        });
    }

    fn add_token_with_peek(
        tokens: &mut VecDeque<Token>,
        it: &mut Peekable<impl Iterator<Item = (usize, char)>>,
        start: usize,
        condition_char: char,
        then_ty: TokenType,
        else_ty: TokenType,
    ) {
        if let Some((end, c)) = it.peek().cloned() {
            if condition_char == c {
                it.next();
                add_token(tokens, start, end, then_ty);
                return;
            }
        }
        add_token(tokens, start, start, else_ty);
    }

    fn add_token_with_require(
        input: &str,
        tokens: &mut VecDeque<Token>,
        it: &mut Peekable<impl Iterator<Item = (usize, char)>>,
        start: usize,
        require_char: char,
        then_ty: TokenType,
        else_msg: &str,
    ) -> Result<(), ExecError> {
        if let Some((end, c)) = it.next() {
            if c == require_char {
                add_token(tokens, start, end, then_ty);
                return Ok(());
            }
        }
        Err(ExecError::PropertyNotLexable(
            String::from(input),
            format!("In position {}, {}", start, else_msg),
        ))
    }

    let mut result = VecDeque::new();

    let mut it = input.chars().enumerate().peekable();
    while let Some((start, c)) = it.next() {
        if c.is_ascii_whitespace() {
            continue;
        }
        let simple_token = match c {
            ',' => Some(TokenType::Comma),
            '(' => Some(TokenType::OpeningBracket(Bracket::Parenthesis)),
            '[' => Some(TokenType::OpeningBracket(Bracket::Square)),
            '{' => Some(TokenType::OpeningBracket(Bracket::Curly)),
            ')' => Some(TokenType::ClosingBracket(Bracket::Parenthesis)),
            ']' => Some(TokenType::ClosingBracket(Bracket::Square)),
            '}' => Some(TokenType::ClosingBracket(Bracket::Curly)),
            _ => None,
        };
        if let Some(simple_token) = simple_token {
            add_token(&mut result, start, start, simple_token);
            continue;
        }

        match c {
            '=' => add_token_with_require(
                input,
                &mut result,
                &mut it,
                start,
                '=',
                TokenType::Comparison(ComparisonType::Eq),
                "a single '=' cannot be used",
            )?,
            '!' => add_token_with_peek(
                &mut result,
                &mut it,
                start,
                '=',
                TokenType::Comparison(ComparisonType::Ne),
                TokenType::ExclamationMark,
            ),
            '<' => add_token_with_peek(
                &mut result,
                &mut it,
                start,
                '=',
                TokenType::Comparison(ComparisonType::Le),
                TokenType::Comparison(ComparisonType::Lt),
            ),
            '>' => add_token_with_peek(
                &mut result,
                &mut it,
                start,
                '=',
                TokenType::Comparison(ComparisonType::Ge),
                TokenType::Comparison(ComparisonType::Gt),
            ),
            '&' => add_token_with_require(
                input,
                &mut result,
                &mut it,
                start,
                '&',
                TokenType::LogicAnd,
                "a single '&' cannot be used",
            )?,
            '|' => add_token_with_require(
                input,
                &mut result,
                &mut it,
                start,
                '|',
                TokenType::LogicOr,
                "a single '|' cannot be used",
            )?,
            'A'..='Z' | 'a'..='z' | '_' => {
                let mut ident = String::from(c);
                let mut end_index = start;
                while let Some((index, c)) = it.peek().copied() {
                    match c {
                        'A'..='Z' | 'a'..='z' | '_' | '0'..='9' => {
                            ident.push(c);
                            it.next();
                        }
                        _ => break,
                    };
                    end_index = index;
                }

                let token_type = if let Some((_, '!')) = it.peek().copied() {
                    // consume the bang but do not add it to the ident
                    it.next();
                    TokenType::MacroInvocation(ident)
                } else {
                    TokenType::Ident(ident)
                };

                add_token(&mut result, start, end_index, token_type);
            }
            '0'..='9' | '-' | '+' => {
                let mut str_val = String::new();
                let hexadecimal = if let Some((_, 'x')) = it.peek() {
                    it.next();
                    true
                } else {
                    str_val.push(c);
                    false
                };

                let mut end_index = start;
                while let Some((index, c)) = it.peek().copied() {
                    match c {
                        '0'..='9' => {
                            it.next();
                            str_val.push(c);
                        }
                        'A'..='F' | 'a'..='f' => {
                            if hexadecimal {
                                it.next();
                                str_val.push(c);
                            } else {
                                break;
                            }
                        }
                        _ => break,
                    };
                    end_index = index;
                }

                let unsigned_val: Result<u64, _> =
                    u64::from_str_radix(&str_val, if hexadecimal { 16 } else { 10 });
                let val = if let Ok(unsigned_val) = unsigned_val {
                    Some(unsigned_val)
                } else if !hexadecimal {
                    let signed_val: Result<i64, _> = str_val.parse();
                    if let Ok(signed_val) = signed_val {
                        Some(signed_val as u64)
                    } else {
                        None
                    }
                } else {
                    None
                };

                let Some(val) = val else {
                    return Err(ExecError::PropertyNotLexable(
                        String::from(input),
                        format!(
                            "Unparsable number at {}..={}: '{}'",
                            start, end_index, str_val
                        ),
                    ));
                };
                add_token(&mut result, start, end_index, TokenType::Number(val));
            }
            _ => {
                return Err(ExecError::PropertyNotLexable(
                    String::from(input),
                    format!("Unknown character in position {}: '{}'", start, c),
                ))
            }
        }
    }
    Ok(result)
}