Calculadora CLI em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir uma calculadora interativa de linha de comando em Zig. Este é um excelente primeiro projeto porque cobre conceitos fundamentais: leitura de entrada do usuário, parsing de strings, tratamento de erros e organização de código.
O Que Vamos Construir
Nossa calculadora vai:
- Aceitar expressões matemáticas como
3 + 4ou10 / 2 - Suportar as quatro operações básicas (+, -, *, /)
- Manter um histórico dos últimos cálculos
- Tratar erros como divisão por zero e entrada inválida
- Rodar em loop até o usuário digitar
sair
Por Que Este Projeto?
Uma calculadora CLI é o “Hello World” dos projetos reais. Ela força você a lidar com problemas que não existem em exemplos triviais: parsing de entrada do usuário (que pode conter qualquer coisa), tratamento robusto de erros e loop de interação. Em Zig, isso nos dá a oportunidade de explorar o sistema de erros da linguagem, que é um dos seus diferenciais.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com conceitos básicos de Zig (fundamentos)
Passo 1: Estrutura do Projeto
Crie o diretório do projeto e o arquivo de build:
mkdir calculadora-cli
cd calculadora-cli
zig init
Isso cria a estrutura padrão. Vamos trabalhar no arquivo src/main.zig.
Passo 2: Definindo os Tipos
Começamos definindo os tipos que representam nossa calculadora. Em Zig, é uma boa prática definir tipos explícitos em vez de usar primitivos soltos — isso torna o código mais legível e seguro.
const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;
/// Operações matemáticas suportadas pela calculadora.
/// Usamos um enum porque o conjunto de operações é fixo e conhecido
/// em tempo de compilação — isso permite que o compilador nos avise
/// se esquecermos de tratar algum caso em um switch.
const Operacao = enum {
soma,
subtracao,
multiplicacao,
divisao,
/// Converte um caractere para a operação correspondente.
/// Retorna erro se o caractere não for um operador válido.
pub fn deCaractere(c: u8) !Operacao {
return switch (c) {
'+' => .soma,
'-' => .subtracao,
'*' => .multiplicacao,
'/' => .divisao,
else => error.OperadorInvalido,
};
}
/// Retorna o símbolo da operação para exibição.
pub fn simbolo(self: Operacao) u8 {
return switch (self) {
.soma => '+',
.subtracao => '-',
.multiplicacao => '*',
.divisao => '/',
};
}
};
/// Representa uma expressão matemática parseada.
/// Separamos o parsing da execução para facilitar testes
/// e permitir exibir a expressão formatada.
const Expressao = struct {
operando_a: f64,
operacao: Operacao,
operando_b: f64,
};
/// Erros possíveis durante o parsing e execução.
const CalcError = error{
OperadorInvalido,
NumeroInvalido,
DivisaoPorZero,
ExpressaoIncompleta,
FormatoInvalido,
};
Por que usar um enum para operações? Porque o compilador Zig nos obriga a tratar todos os casos em um switch. Se adicionarmos uma nova operação no futuro, o compilador vai nos avisar de todos os lugares que precisam ser atualizados. Isso é muito mais seguro do que comparar caracteres soltos.
Passo 3: Parser de Expressões
Agora implementamos o parser que transforma a string digitada pelo usuário em uma Expressao estruturada.
/// Faz o parsing de uma string de entrada como "3.5 + 2.1"
/// e retorna uma Expressao estruturada.
///
/// Decisão de design: usamos split simples por espaços em vez de
/// um parser mais sofisticado. Para uma calculadora básica, isso é
/// suficiente e mantém o código acessível para iniciantes.
fn parsearExpressao(entrada: []const u8) CalcError!Expressao {
// Remove espaços em branco nas bordas
const limpa = mem.trim(u8, entrada, " \t\r\n");
if (limpa.len == 0) return CalcError.ExpressaoIncompleta;
// Dividimos por espaço — esperamos o formato "A op B"
var partes = mem.splitSequence(u8, limpa, " ");
// Primeiro operando
const str_a = partes.next() orelse return CalcError.ExpressaoIncompleta;
const a = fmt.parseFloat(f64, str_a) catch return CalcError.NumeroInvalido;
// Operador
const str_op = partes.next() orelse return CalcError.ExpressaoIncompleta;
if (str_op.len != 1) return CalcError.OperadorInvalido;
const op = Operacao.deCaractere(str_op[0]) catch return CalcError.OperadorInvalido;
// Segundo operando
const str_b = partes.next() orelse return CalcError.ExpressaoIncompleta;
const b = fmt.parseFloat(f64, str_b) catch return CalcError.NumeroInvalido;
return Expressao{
.operando_a = a,
.operacao = op,
.operando_b = b,
};
}
Por que split por espaços? É a abordagem mais simples e funcional para o nosso caso. Um parser mais robusto (que aceitasse 3+4 sem espaços) exigiria um tokenizer, o que adicionaria complexidade sem benefício proporcional neste projeto introdutório.
Passo 4: Motor de Cálculo
Com a expressão parseada, o cálculo em si é direto:
/// Executa o cálculo representado por uma expressão.
/// Retorna erro apenas em caso de divisão por zero.
fn calcular(expr: Expressao) CalcError!f64 {
return switch (expr.operacao) {
.soma => expr.operando_a + expr.operando_b,
.subtracao => expr.operando_a - expr.operando_b,
.multiplicacao => expr.operando_a * expr.operando_b,
.divisao => {
if (expr.operando_b == 0.0) return CalcError.DivisaoPorZero;
return expr.operando_a / expr.operando_b;
},
};
}
Por que verificar divisão por zero explicitamente? Em muitas linguagens, dividir por zero com floats retorna Infinity ou NaN. Embora Zig também faça isso com f64, preferimos tratar como erro explícito para dar feedback claro ao usuário. Essa é uma decisão de UX, não de correção matemática.
Passo 5: Histórico de Operações
Vamos adicionar um histórico para que o usuário possa rever cálculos anteriores. Usamos um buffer de tamanho fixo para evitar alocação dinâmica — uma prática comum e idiomática em Zig.
/// Registro de uma operação realizada.
const RegistroHistorico = struct {
expressao: [64]u8,
expressao_len: usize,
resultado: f64,
};
/// Histórico circular de operações.
/// Usamos um buffer fixo em vez de ArrayList porque:
/// 1. Não precisamos de alocação dinâmica
/// 2. O tamanho máximo é conhecido e pequeno
/// 3. O comportamento circular (sobrescrever antigos) é desejável
const Historico = struct {
registros: [20]RegistroHistorico,
quantidade: usize,
proximo_indice: usize,
const Self = @This();
pub fn init() Self {
return Self{
.registros = undefined,
.quantidade = 0,
.proximo_indice = 0,
};
}
pub fn adicionar(self: *Self, expr: Expressao, resultado: f64) void {
var buf: [64]u8 = undefined;
const texto = fmt.bufPrint(&buf, "{d} {c} {d} = {d:.4}", .{
expr.operando_a,
expr.operacao.simbolo(),
expr.operando_b,
resultado,
}) catch return;
self.registros[self.proximo_indice] = .{
.expressao = buf,
.expressao_len = texto.len,
.resultado = resultado,
};
self.proximo_indice = (self.proximo_indice + 1) % 20;
if (self.quantidade < 20) self.quantidade += 1;
}
pub fn exibir(self: *const Self, writer: anytype) !void {
if (self.quantidade == 0) {
try writer.print(" (nenhum cálculo realizado ainda)\n", .{});
return;
}
var i: usize = 0;
while (i < self.quantidade) : (i += 1) {
const idx = if (self.quantidade < 20)
i
else
(self.proximo_indice + i) % 20;
const reg = self.registros[idx];
try writer.print(" {s}\n", .{reg.expressao[0..reg.expressao_len]});
}
}
};
Passo 6: Formatação de Erros
Uma boa calculadora precisa de mensagens de erro claras:
/// Retorna uma mensagem amigável para cada tipo de erro.
/// Decisão: mensagens em português e descritivas, porque o público
/// é brasileiro e mensagens técnicas em inglês não ajudam um iniciante.
fn mensagemErro(err: CalcError) []const u8 {
return switch (err) {
CalcError.OperadorInvalido => "Operador inválido. Use +, -, * ou /",
CalcError.NumeroInvalido => "Número inválido. Use números como 3, 3.14 ou -2",
CalcError.DivisaoPorZero => "Erro: divisão por zero não é permitida",
CalcError.ExpressaoIncompleta => "Expressão incompleta. Use o formato: 3 + 4",
CalcError.FormatoInvalido => "Formato inválido. Use o formato: número operador número",
};
}
Passo 7: Loop Principal
Finalmente, montamos o loop interativo que une tudo:
pub fn main() !void {
const stdout = io.getStdOut().writer();
const stdin = io.getStdIn().reader();
var historico = Historico.init();
// Banner inicial
try stdout.print(
\\
\\╔══════════════════════════════════════╗
\\║ Calculadora Zig v1.0 ║
\\║ Digite uma expressão: 3 + 4 ║
\\║ Comandos: historico, limpar, sair ║
\\╚══════════════════════════════════════╝
\\
, .{});
// Buffer para leitura de entrada
var buf: [256]u8 = undefined;
while (true) {
try stdout.print("\n> ", .{});
// Lê uma linha da entrada padrão
const linha = stdin.readUntilDelimiterOrEof(&buf, '\n') catch {
try stdout.print("Erro ao ler entrada.\n", .{});
continue;
} orelse break; // EOF = sair
const entrada = mem.trim(u8, linha, " \t\r");
// Comandos especiais
if (mem.eql(u8, entrada, "sair") or mem.eql(u8, entrada, "quit")) {
try stdout.print("Até logo!\n", .{});
break;
}
if (mem.eql(u8, entrada, "historico")) {
try stdout.print("\n--- Histórico ---\n", .{});
try historico.exibir(stdout);
continue;
}
if (mem.eql(u8, entrada, "limpar")) {
historico = Historico.init();
try stdout.print("Histórico limpo.\n", .{});
continue;
}
if (entrada.len == 0) continue;
// Parsear e calcular
const expr = parsearExpressao(entrada) catch |err| {
try stdout.print(" {s}\n", .{mensagemErro(err)});
continue;
};
const resultado = calcular(expr) catch |err| {
try stdout.print(" {s}\n", .{mensagemErro(err)});
continue;
};
try stdout.print(" = {d:.6}\n", .{resultado});
historico.adicionar(expr, resultado);
}
}
Passo 8: Testes
Uma das grandes vantagens de Zig é o suporte nativo a testes. Vamos adicionar testes para nossas funções:
test "parsear expressão válida" {
const expr = try parsearExpressao("3 + 4");
try std.testing.expectEqual(@as(f64, 3.0), expr.operando_a);
try std.testing.expectEqual(Operacao.soma, expr.operacao);
try std.testing.expectEqual(@as(f64, 4.0), expr.operando_b);
}
test "parsear expressão com decimais" {
const expr = try parsearExpressao("3.14 * 2.0");
try std.testing.expectApproxEqAbs(@as(f64, 3.14), expr.operando_a, 0.001);
try std.testing.expectEqual(Operacao.multiplicacao, expr.operacao);
}
test "rejeitar operador inválido" {
const resultado = parsearExpressao("3 ^ 4");
try std.testing.expectError(CalcError.OperadorInvalido, resultado);
}
test "rejeitar expressão incompleta" {
const resultado = parsearExpressao("3 +");
try std.testing.expectError(CalcError.ExpressaoIncompleta, resultado);
}
test "calcular soma" {
const expr = Expressao{ .operando_a = 10, .operacao = .soma, .operando_b = 5 };
const resultado = try calcular(expr);
try std.testing.expectEqual(@as(f64, 15.0), resultado);
}
test "rejeitar divisão por zero" {
const expr = Expressao{ .operando_a = 10, .operacao = .divisao, .operando_b = 0 };
try std.testing.expectError(CalcError.DivisaoPorZero, calcular(expr));
}
Execute os testes com:
zig build test
Compilando e Executando
# Compilar
zig build
# Executar
./zig-out/bin/calculadora-cli
# Ou compilar e executar diretamente
zig run src/main.zig
Exemplo de Uso
╔══════════════════════════════════════╗
║ Calculadora Zig v1.0 ║
║ Digite uma expressão: 3 + 4 ║
║ Comandos: historico, limpar, sair ║
╚══════════════════════════════════════╝
> 3 + 4
= 7.000000
> 10 / 3
= 3.333333
> 5 * 0.5
= 2.500000
> 10 / 0
Erro: divisão por zero não é permitida
> historico
--- Histórico ---
3 + 4 = 7.0000
10 / 3 = 3.3333
5 * 0.5 = 2.5000
> sair
Até logo!
Desafios Extras
Quer ir além? Tente estas melhorias:
- Suporte a parênteses — implemente parsing de expressões como
(3 + 4) * 2 - Funções matemáticas — adicione
sqrt,pow,sin,cos - Variável
ans— use o resultado anterior como operando - Persistência — salve o histórico em arquivo usando I/O de arquivos
Conceitos Aprendidos
- Leitura de entrada do usuário com
stdin - Parsing de strings com
std.memestd.fmt - Tratamento de erros com
catche tipos de erro personalizados - Uso de
enumestructpara modelar domínio - Buffers de tamanho fixo vs alocação dinâmica
- Testes unitários nativos
Próximos Passos
- Aprenda mais sobre manipulação de strings em Zig
- Explore a stdlib std.fmt para formatação avançada
- Construa o próximo projeto: Todo List CLI