Calculadora com Parser em Rust

Construa uma calculadora interativa em Rust com tokenizador, parser de descida recursiva, variáveis e funções matemáticas como sin e cos.

Construir uma calculadora pode parecer simples, mas quando adicionamos variáveis, funções matemáticas e precedência de operadores, o projeto se torna um excelente exercício de design de linguagens. Neste walkthrough, vamos construir uma calculadora completa com um tokenizador (lexer), um parser de descida recursiva que respeita a precedência dos operadores, e uma interface REPL interativa.

Este projeto ensina conceitos fundamentais de compiladores — tokenização, parsing e avaliação — em um contexto acessível e prático. A técnica de descida recursiva que implementaremos aqui é a mesma usada em muitos compiladores e interpretadores reais.

O Que Vamos Construir

Uma calculadora interativa com as seguintes funcionalidades:

  • Operações aritméticas: +, -, *, /
  • Parênteses para agrupamento
  • Números inteiros e decimais (ponto flutuante)
  • Variáveis definidas pelo usuário (x = 42)
  • Funções matemáticas: sin, cos, sqrt, abs, ln
  • Operador unário de negação (-5)
  • Interface REPL com histórico de variáveis

Estrutura do Projeto

calculadora/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── lexer.rs
    ├── parser.rs
    └── avaliador.rs

Configurando o Projeto

cargo new calculadora
cd calculadora
[package]
name = "calculadora"
version = "0.1.0"
edition = "2021"

Não precisamos de dependências externas — a biblioteca padrão fornece tudo que precisamos.

Passo 1: O Tokenizador (Lexer)

O tokenizador transforma a string de entrada em uma sequência de tokens tipados. Cada token representa uma unidade lógica: um número, um operador, um identificador etc.

Crie o arquivo src/lexer.rs:

/// Tipos de token reconhecidos pela calculadora
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    Numero(f64),
    Identificador(String),
    Mais,         // +
    Menos,        // -
    Asterisco,    // *
    Barra,        // /
    AbreParentese,  // (
    FechaParentese, // )
    Igual,        // =
    Virgula,      // ,
    FimDaEntrada,
}

impl std::fmt::Display for Token {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Token::Numero(n) => write!(f, "{}", n),
            Token::Identificador(nome) => write!(f, "{}", nome),
            Token::Mais => write!(f, "+"),
            Token::Menos => write!(f, "-"),
            Token::Asterisco => write!(f, "*"),
            Token::Barra => write!(f, "/"),
            Token::AbreParentese => write!(f, "("),
            Token::FechaParentese => write!(f, ")"),
            Token::Igual => write!(f, "="),
            Token::Virgula => write!(f, ","),
            Token::FimDaEntrada => write!(f, "EOF"),
        }
    }
}

/// Transforma uma string em uma sequência de tokens
pub fn tokenizar(entrada: &str) -> Result<Vec<Token>, String> {
    let mut tokens = Vec::new();
    let caracteres: Vec<char> = entrada.chars().collect();
    let mut pos = 0;

    while pos < caracteres.len() {
        let c = caracteres[pos];

        // Pular espaços em branco
        if c.is_whitespace() {
            pos += 1;
            continue;
        }

        // Números (inteiros e decimais)
        if c.is_ascii_digit() || (c == '.' && pos + 1 < caracteres.len()
            && caracteres[pos + 1].is_ascii_digit())
        {
            let inicio = pos;
            while pos < caracteres.len()
                && (caracteres[pos].is_ascii_digit() || caracteres[pos] == '.')
            {
                pos += 1;
            }
            let texto: String = caracteres[inicio..pos].iter().collect();
            let numero: f64 = texto
                .parse()
                .map_err(|_| format!("Número inválido: '{}'", texto))?;
            tokens.push(Token::Numero(numero));
            continue;
        }

        // Identificadores (variáveis e funções)
        if c.is_ascii_alphabetic() || c == '_' {
            let inicio = pos;
            while pos < caracteres.len()
                && (caracteres[pos].is_ascii_alphanumeric() || caracteres[pos] == '_')
            {
                pos += 1;
            }
            let nome: String = caracteres[inicio..pos].iter().collect();
            tokens.push(Token::Identificador(nome));
            continue;
        }

        // Operadores e pontuação
        let token = match c {
            '+' => Token::Mais,
            '-' => Token::Menos,
            '*' => Token::Asterisco,
            '/' => Token::Barra,
            '(' => Token::AbreParentese,
            ')' => Token::FechaParentese,
            '=' => Token::Igual,
            ',' => Token::Virgula,
            _ => return Err(format!("Caractere inesperado: '{}'", c)),
        };
        tokens.push(token);
        pos += 1;
    }

    tokens.push(Token::FimDaEntrada);
    Ok(tokens)
}

#[cfg(test)]
mod testes {
    use super::*;

    #[test]
    fn testar_expressao_simples() {
        let tokens = tokenizar("3 + 4.5 * 2").unwrap();
        assert_eq!(tokens[0], Token::Numero(3.0));
        assert_eq!(tokens[1], Token::Mais);
        assert_eq!(tokens[2], Token::Numero(4.5));
        assert_eq!(tokens[3], Token::Asterisco);
        assert_eq!(tokens[4], Token::Numero(2.0));
    }

    #[test]
    fn testar_atribuicao() {
        let tokens = tokenizar("x = 10").unwrap();
        assert_eq!(tokens[0], Token::Identificador("x".to_string()));
        assert_eq!(tokens[1], Token::Igual);
        assert_eq!(tokens[2], Token::Numero(10.0));
    }
}

Passo 2: O Parser de Descida Recursiva

O parser implementa uma gramática com precedência de operadores usando descida recursiva. A gramática, em notação simplificada, é:

expressao    → atribuicao
atribuicao   → identificador '=' expressao | soma
soma         → multiplicacao (('+' | '-') multiplicacao)*
multiplicacao → unario (('*' | '/') unario)*
unario       → '-' unario | chamada
chamada      → identificador '(' argumentos ')' | primario
primario     → NUMERO | identificador | '(' expressao ')'

Crie o arquivo src/parser.rs:

use crate::lexer::Token;

/// Nó da árvore sintática abstrata
#[derive(Debug, Clone)]
pub enum Expressao {
    Numero(f64),
    Variavel(String),
    Negacao(Box<Expressao>),
    Operacao {
        esquerda: Box<Expressao>,
        operador: Operador,
        direita: Box<Expressao>,
    },
    Atribuicao {
        nome: String,
        valor: Box<Expressao>,
    },
    ChamadaFuncao {
        nome: String,
        argumentos: Vec<Expressao>,
    },
}

#[derive(Debug, Clone)]
pub enum Operador {
    Somar,
    Subtrair,
    Multiplicar,
    Dividir,
}

pub struct Parser {
    tokens: Vec<Token>,
    posicao: usize,
}

impl Parser {
    pub fn new(tokens: Vec<Token>) -> Self {
        Self { tokens, posicao: 0 }
    }

    fn atual(&self) -> &Token {
        &self.tokens[self.posicao]
    }

    fn avancar(&mut self) -> Token {
        let token = self.tokens[self.posicao].clone();
        if self.posicao < self.tokens.len() - 1 {
            self.posicao += 1;
        }
        token
    }

    fn esperar(&mut self, esperado: &Token) -> Result<(), String> {
        if self.atual() == esperado {
            self.avancar();
            Ok(())
        } else {
            Err(format!(
                "Esperado '{}', encontrado '{}'",
                esperado,
                self.atual()
            ))
        }
    }

    pub fn analisar(&mut self) -> Result<Expressao, String> {
        let resultado = self.expressao()?;
        if *self.atual() != Token::FimDaEntrada {
            return Err(format!("Token inesperado: '{}'", self.atual()));
        }
        Ok(resultado)
    }

    fn expressao(&mut self) -> Result<Expressao, String> {
        self.atribuicao()
    }

    fn atribuicao(&mut self) -> Result<Expressao, String> {
        // Verifica se é uma atribuição: identificador = expressao
        if let Token::Identificador(nome) = self.atual().clone() {
            if self.posicao + 1 < self.tokens.len()
                && self.tokens[self.posicao + 1] == Token::Igual
            {
                self.avancar(); // consome identificador
                self.avancar(); // consome '='
                let valor = self.expressao()?;
                return Ok(Expressao::Atribuicao {
                    nome,
                    valor: Box::new(valor),
                });
            }
        }
        self.soma()
    }

    fn soma(&mut self) -> Result<Expressao, String> {
        let mut esquerda = self.multiplicacao()?;

        loop {
            let operador = match self.atual() {
                Token::Mais => Operador::Somar,
                Token::Menos => Operador::Subtrair,
                _ => break,
            };
            self.avancar();
            let direita = self.multiplicacao()?;
            esquerda = Expressao::Operacao {
                esquerda: Box::new(esquerda),
                operador,
                direita: Box::new(direita),
            };
        }

        Ok(esquerda)
    }

    fn multiplicacao(&mut self) -> Result<Expressao, String> {
        let mut esquerda = self.unario()?;

        loop {
            let operador = match self.atual() {
                Token::Asterisco => Operador::Multiplicar,
                Token::Barra => Operador::Dividir,
                _ => break,
            };
            self.avancar();
            let direita = self.unario()?;
            esquerda = Expressao::Operacao {
                esquerda: Box::new(esquerda),
                operador,
                direita: Box::new(direita),
            };
        }

        Ok(esquerda)
    }

    fn unario(&mut self) -> Result<Expressao, String> {
        if *self.atual() == Token::Menos {
            self.avancar();
            let operando = self.unario()?;
            return Ok(Expressao::Negacao(Box::new(operando)));
        }
        self.chamada()
    }

    fn chamada(&mut self) -> Result<Expressao, String> {
        if let Token::Identificador(nome) = self.atual().clone() {
            if self.posicao + 1 < self.tokens.len()
                && self.tokens[self.posicao + 1] == Token::AbreParentese
            {
                self.avancar(); // consome identificador
                self.avancar(); // consome '('
                let mut argumentos = Vec::new();

                if *self.atual() != Token::FechaParentese {
                    argumentos.push(self.expressao()?);
                    while *self.atual() == Token::Virgula {
                        self.avancar();
                        argumentos.push(self.expressao()?);
                    }
                }

                self.esperar(&Token::FechaParentese)?;
                return Ok(Expressao::ChamadaFuncao { nome, argumentos });
            }
        }
        self.primario()
    }

    fn primario(&mut self) -> Result<Expressao, String> {
        match self.atual().clone() {
            Token::Numero(valor) => {
                self.avancar();
                Ok(Expressao::Numero(valor))
            }
            Token::Identificador(nome) => {
                self.avancar();
                Ok(Expressao::Variavel(nome))
            }
            Token::AbreParentese => {
                self.avancar();
                let expr = self.expressao()?;
                self.esperar(&Token::FechaParentese)?;
                Ok(expr)
            }
            outro => Err(format!("Expressão esperada, encontrado '{}'", outro)),
        }
    }
}

Passo 3: O Avaliador

O avaliador percorre a AST e calcula o resultado de cada expressão, mantendo um ambiente com as variáveis definidas.

Crie o arquivo src/avaliador.rs:

use std::collections::HashMap;
use crate::parser::{Expressao, Operador};

/// Ambiente de execução com variáveis e funções
pub struct Ambiente {
    variaveis: HashMap<String, f64>,
}

impl Ambiente {
    pub fn new() -> Self {
        let mut variaveis = HashMap::new();
        // Constantes pré-definidas
        variaveis.insert("pi".to_string(), std::f64::consts::PI);
        variaveis.insert("e".to_string(), std::f64::consts::E);

        Self { variaveis }
    }

    /// Avalia uma expressão e retorna o resultado
    pub fn avaliar(&mut self, expr: &Expressao) -> Result<f64, String> {
        match expr {
            Expressao::Numero(valor) => Ok(*valor),

            Expressao::Variavel(nome) => {
                self.variaveis
                    .get(nome)
                    .copied()
                    .ok_or_else(|| format!("Variável não definida: '{}'", nome))
            }

            Expressao::Negacao(operando) => {
                let valor = self.avaliar(operando)?;
                Ok(-valor)
            }

            Expressao::Operacao {
                esquerda,
                operador,
                direita,
            } => {
                let val_esq = self.avaliar(esquerda)?;
                let val_dir = self.avaliar(direita)?;

                match operador {
                    Operador::Somar => Ok(val_esq + val_dir),
                    Operador::Subtrair => Ok(val_esq - val_dir),
                    Operador::Multiplicar => Ok(val_esq * val_dir),
                    Operador::Dividir => {
                        if val_dir == 0.0 {
                            Err("Erro: divisão por zero".to_string())
                        } else {
                            Ok(val_esq / val_dir)
                        }
                    }
                }
            }

            Expressao::Atribuicao { nome, valor } => {
                let resultado = self.avaliar(valor)?;
                self.variaveis.insert(nome.clone(), resultado);
                Ok(resultado)
            }

            Expressao::ChamadaFuncao { nome, argumentos } => {
                self.chamar_funcao(nome, argumentos)
            }
        }
    }

    fn chamar_funcao(
        &mut self,
        nome: &str,
        argumentos: &[Expressao],
    ) -> Result<f64, String> {
        // Funções de um argumento
        if argumentos.len() == 1 {
            let arg = self.avaliar(&argumentos[0])?;
            return match nome {
                "sin" | "sen" => Ok(arg.sin()),
                "cos" => Ok(arg.cos()),
                "tan" => Ok(arg.tan()),
                "sqrt" | "raiz" => {
                    if arg < 0.0 {
                        Err("Erro: raiz quadrada de número negativo".to_string())
                    } else {
                        Ok(arg.sqrt())
                    }
                }
                "abs" => Ok(arg.abs()),
                "ln" => {
                    if arg <= 0.0 {
                        Err("Erro: ln de valor não positivo".to_string())
                    } else {
                        Ok(arg.ln())
                    }
                }
                "log" => {
                    if arg <= 0.0 {
                        Err("Erro: log de valor não positivo".to_string())
                    } else {
                        Ok(arg.log10())
                    }
                }
                "ceil" => Ok(arg.ceil()),
                "floor" => Ok(arg.floor()),
                "round" => Ok(arg.round()),
                _ => Err(format!("Função desconhecida: '{}'", nome)),
            };
        }

        // Funções de dois argumentos
        if argumentos.len() == 2 {
            let arg1 = self.avaliar(&argumentos[0])?;
            let arg2 = self.avaliar(&argumentos[1])?;
            return match nome {
                "pow" => Ok(arg1.powf(arg2)),
                "max" => Ok(arg1.max(arg2)),
                "min" => Ok(arg1.min(arg2)),
                _ => Err(format!("Função '{}' não aceita 2 argumentos", nome)),
            };
        }

        Err(format!(
            "Função '{}' chamada com {} argumentos (esperado 1 ou 2)",
            nome,
            argumentos.len()
        ))
    }
}

Passo 4: Juntando Tudo com a Interface REPL

Agora vamos criar a interface interativa em src/main.rs:

mod avaliador;
mod lexer;
mod parser;

use std::io::{self, BufRead, Write};

fn main() {
    println!("=== Calculadora Rust ===");
    println!("Digite expressões matemáticas para avaliar.");
    println!("Exemplos: 2 + 3 * 4, x = 10, sqrt(x), sin(pi/2)");
    println!("Constantes disponíveis: pi, e");
    println!("Funções: sin, cos, tan, sqrt, abs, ln, log, ceil, floor, round, pow, max, min");
    println!("Digite 'sair' para encerrar.\n");

    let stdin = io::stdin();
    let mut ambiente = avaliador::Ambiente::new();

    loop {
        print!("calc> ");
        io::stdout().flush().unwrap();

        let mut linha = String::new();
        if stdin.lock().read_line(&mut linha).unwrap() == 0 {
            break;
        }
        let entrada = linha.trim();

        if entrada.is_empty() {
            continue;
        }

        if entrada == "sair" {
            println!("Até logo!");
            break;
        }

        // Tokenizar
        let tokens = match lexer::tokenizar(entrada) {
            Ok(t) => t,
            Err(e) => {
                println!("Erro léxico: {}", e);
                continue;
            }
        };

        // Analisar (parser)
        let mut analisador = parser::Parser::new(tokens);
        let expressao = match analisador.analisar() {
            Ok(expr) => expr,
            Err(e) => {
                println!("Erro sintático: {}", e);
                continue;
            }
        };

        // Avaliar
        match ambiente.avaliar(&expressao) {
            Ok(resultado) => {
                // Formata o resultado de forma legível
                if resultado == resultado.floor() && resultado.abs() < 1e15 {
                    println!("= {}", resultado as i64);
                } else {
                    println!("= {:.6}", resultado);
                }
            }
            Err(e) => println!("{}", e),
        }
    }
}

Como Executar

# Compilar e executar
cargo run

# Sessão de exemplo:
calc> 2 + 3 * 4
= 14

calc> (2 + 3) * 4
= 20

calc> x = 10
= 10

calc> y = 20
= 20

calc> x + y
= 30

calc> sqrt(x * x + y * y)
= 22.360680

calc> sin(pi / 2)
= 1

calc> raio = 5
= 5

calc> area = pi * raio * raio
= 78.539816

calc> pow(2, 10)
= 1024

calc> -3 + 4
= 1

# Executar os testes
cargo test

Desafios para Expandir

  1. Operador de potenciação: Adicione o operador ^ para potenciação com associatividade à direita (ex: 2^3^2 deve ser 2^(3^2) = 512).
  2. Histórico de expressões: Implemente uma variável especial ans que armazena o resultado da última avaliação, permitindo encadear cálculos.
  3. Funções definidas pelo usuário: Permita que o usuário defina funções como f(x) = x * x + 1, armazenando-as no ambiente.
  4. Formatação numérica: Adicione comandos como hex, bin e oct para exibir resultados em diferentes bases numéricas.
  5. Tratamento de erros avançado: Mostre a posição exata do erro na expressão com um indicador visual, como 2 + * 3 mostrando ^-- operando esperado.

Veja Também