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
- Operador de potenciação: Adicione o operador
^para potenciação com associatividade à direita (ex:2^3^2deve ser2^(3^2) = 512). - Histórico de expressões: Implemente uma variável especial
ansque armazena o resultado da última avaliação, permitindo encadear cálculos. - Funções definidas pelo usuário: Permita que o usuário defina funções como
f(x) = x * x + 1, armazenando-as no ambiente. - Formatação numérica: Adicione comandos como
hex,bineoctpara exibir resultados em diferentes bases numéricas. - Tratamento de erros avançado: Mostre a posição exata do erro na expressão com um indicador visual, como
2 + * 3mostrando^-- operando esperado.
Veja Também
- Trabalhando com Strings — parsing e manipulação de texto
- HashMap: Tabelas Hash — armazenamento de variáveis
- Vec: Vetores Dinâmicos — lista de tokens e nós da AST
- Entrada e Saída Padrão — interface REPL
- Como Ler Input do Usuário — leitura interativa no terminal