ADRIANOLAUREANO← Artigos

Construindo uma linguagem de programação do zero em Rust

Descubra como um computador entende o código que escrevemos construindo a Pulso, uma linguagem de programação completamente funcional em Rust.

09artigo da série
7etapas do pipeline
14arquivos completos
0dependências externas

01 / UMA FRASE QUE A CPU NÃO ENTENDE

Escrevemos uma linha. Quem explica ao computador o que ela significa?

mostre("Olá, mundo");

Mesmo sem conhecer nossa futura linguagem, provavelmente você consegue imaginar o resultado: a frase Olá, mundo deve aparecer na tela. A linha parece simples porque nós reconhecemos palavras, aspas e pontuação.

A CPU não faz essa leitura. Ela não sabe que mostre significa “escreva na tela”. Não sabe que as aspas protegem um texto, que os parênteses entregam esse texto à instrução ou que o ponto e vírgula encerra a frase. Para ela, o arquivo ainda é apenas uma sequência de números.

NÓS ENXERGAMOSuma instruçãomostrar uma mensagem
O ARQUIVO GUARDAcaracteresm · o · s · t · r · e · ( · ...
A CPU RECEBEnúmerosbytes codificados em UTF-8
ESTÁ FALTANDOum tradutorregras que deem sentido ao texto

Uma linguagem de programação não é uma força escondida dentro das palavras. Ela é um acordo: nós escrevemos seguindo determinadas regras e outro programa implementa essas regras. É esse programa que construiremos.

A PERGUNTA DO ARTIGO

Como transformar mostre("Olá, mundo"); em uma ação real sem pular nenhuma etapa?

02 / A SÉRIE CHEGOU ÀS LINGUAGENS

Cada artigo anterior entregou uma peça desta construção.

ARQUIVOSbytes persistentes
FORMATOSestrutura e validação
SISTEMAarmazenamento e execução
DADOSíndices e valores
GITárvores e histórico
LINGUAGEMtexto vira comportamento

Nos artigos anteriores vimos que um arquivo não carrega seu significado dentro dos bytes. Um PNG só vira imagem porque um programa conhece suas regras. Umbanco de dados só encontra um registro porque alguém projetou páginas e índices. O Rastro só recupera uma versão porque sabe relacionar objetos e identificadores.

A diferença é que agora o arquivo não descreve uma imagem, um banco ou um commit. Ele descreve ações que ainda acontecerão. Em vez de começar decorando nomes como lexer, parser e AST, vamos encontrá-los no caminho, exatamente no momento em que cada um resolver um problema nosso.

03 / A MENOR SOLUÇÃO QUE PODERIA FUNCIONAR

E se o nosso programa reconhecesse a frase inteira?

Antes de projetar uma linguagem, vamos fazer algo deliberadamente pequeno. Nosso primeiro interpretador recebe o conteúdo do arquivo e compara com a única frase que conhece.

fn executar(fonte: &str) {
    if fonte.trim() == r#"mostre("Olá, mundo");"# {
        println!("Olá, mundo");
    }
}

fonte é o texto escrito pela pessoa.trim() remove espaços e quebras de linha das extremidades. Oif compara o arquivo inteiro com uma frase fixa. Se forem iguais, Rust imprime a mensagem.

ENTRADA CONHECIDAmostre("Olá, mundo");funciona
QUALQUER VARIAÇÃOmostre("Oi");não produz nada

Conseguimos executar um arquivo. Mas ainda não construímos uma linguagem: construímos uma fechadura que aceita uma única chave. Trocar a mensagem, inserir dois espaços ou escrever duas instruções já quebra nossa solução.

O primeiro problema real aparece

Não podemos comparar o programa inteiro. Precisamos reconhecer a parte que manda mostrar, a pontuação que organiza a chamada e o texto que pode mudar. Pela primeira vez, faz sentido separar o código em peças.

INSTRUÇÃOmostrequal ação executar
ABERTURA(começo do argumento
DADO"Olá, mundo"o texto pode mudar
FINAL);fecha e encerra

Essas peças ainda não precisam de nomes técnicos. Primeiro vamos batizar o projeto e decidir qual será a próxima capacidade dele.

04 / NOSSA LINGUAGEM PRECISA DE IDENTIDADE

O texto começa imóvel. Nosso programa lhe dará Pulso.

Chamaremos a linguagem de Pulso porque o arquivo começa imóvel e ganha comportamento quando passa pelo nosso pipeline. A extensão será.pulso, e as palavras principais ficarão em português para que a sintaxe não esconda os conceitos.

mostre("Olá, mundo");

Esta será a primeira versão completa da Pulso. Quando ela funcionar, aparecerá uma nova vontade: guardar um valor para não repeti-lo. Isso fará nascer a variável. Depois desejaremos calcular, escolher um caminho, repetir uma tarefa e dar nome a um comportamento reutilizável.

1 · MOSTRARmostre("Olá")produzir o primeiro efeito
2 · LEMBRARvar nome = "Lia"guardar um valor sob um nome
3 · CALCULAR2 + 3 × 4combinar valores
4 · ESCOLHERse / senaoseguir um de dois caminhos
5 · REPETIRenquantorefazer uma ação
6 · REUTILIZARfuncaodar nome a uma receita

Não jogaremos todos esses recursos na mesa de uma vez. Cada um entrará quando a versão anterior deixar de ser suficiente. É assim que a Pulso deixará de ser uma frase reconhecida por acaso e se tornará uma linguagem de verdade.

05 / ANTES DO TOKEN EXISTE UM CARACTERE

O arquivo chega como bytes; Rust nos entrega caracteres.

Um byte é um número de 0 a 255. Um caractere é uma unidade de texto percebida pela linguagem, como v, á, + ou{. Em UTF-8, alguns caracteres ocupam um byte; outros precisam de vários. Por isso não devemos fingir que “um byte” e “uma letra” são sempre a mesma coisa.

NO DISCObytes UTF-876 61 72 20 61 ...
NO LEXERVec<char>'v' · 'a' · 'r' · espaço · 'a'
pub fn new(source: &str) -> Self {
    Self {
        chars: source.chars().collect(),
        current: 0,
        line: 1,
        column: 1,
    }
}

&str é uma visão sobre texto UTF-8 em Rust.source.chars() percorre os valores Unicode decodificados.collect() reúne esses caracteres em um vetor. O índicecurrent diz qual será o próximo caractere; linha e coluna permitem produzir mensagens úteis quando algo dá errado.

06 / CARACTERES SOZINHOS NÃO CONTAM A HISTÓRIA

Precisamos juntar caracteres que pertencem à mesma ideia.

var idade = 20 + 1;

Depois de mostrar uma mensagem, queremos que a Pulso consiga lembrar valores. Uma variável funciona como uma caixa com etiqueta: guardamos um valor e usamos seu nome para encontrá-lo mais tarde. Nesta linha, calculamos20 + 1 e guardamos o resultado na caixa chamadaidade.

O nome idade possui cinco caracteres, mas deve viajar como uma peça. O número 20 possui dois algarismos, mas representa um único valor. O operador + é outra peça. Chamamos cada unidade reconhecida de token.

Trecho originalClasse do tokenInformação guardada
varpalavra reservadainício de declaração
idadeidentificadoro nome “idade”
=operadoratribuir/inicializar
20númeroo valor 20
+operadorsoma
1númeroo valor 1
;delimitadorfim do comando

Lexema é o pedaço concreto do texto, como “idade”.Token é sua classificação, comoIdentifier("idade"). Dois nomes diferentes possuem lexemas diferentes, mas pertencem à mesma categoria.

Identifier("idade")
Equal
Number(20.0)
Plus
Number(1.0)
Semicolon

07 / CONSTRUINDO O LEITOR DE PEÇAS

Lexer é o programa que transforma caracteres em tokens.

O lexer também é chamado de analisador léxico. “Léxico” lembra vocabulário: nesta etapa queremos reconhecer as palavras e símbolos disponíveis, não decidir se a frase completa faz sentido.

1olharqual é o próximo caractere?
2agruparfaz parte de nome, número ou texto?
3classificarqual TokenKind representa isso?
4localizarem qual linha e coluna começou?
match character {
    '(' => Some(TokenKind::LeftParen),
    '+' => Some(TokenKind::Plus),
    '"' => Some(self.string(line, column)?),
    c if c.is_ascii_digit() => Some(self.number(c, line, column)?),
    c if is_identifier_start(c) => Some(self.identifier(c)),
    c if c.is_whitespace() => None,
    other => return Err(/* erro léxico */),
}

Símbolos de um caractere são reconhecidos imediatamente. Ao encontrar uma aspa, o lexer continua até a aspa final. Ao encontrar um dígito, continua enquanto houver dígitos e talvez uma parte decimal. Ao encontrar uma letra, continua formando um nome e depois verifica se ele é reservado.

Como “enquanto” deixa de ser um nome comum?

match text.as_str() {
    "var" => TokenKind::Var,
    "se" => TokenKind::If,
    "enquanto" => TokenKind::While,
    "funcao" => TokenKind::Function,
    "retorne" => TokenKind::Return,
    _ => TokenKind::Identifier(text),
}

Primeiro lemos uma palavra inteira. Depois consultamos a pequena tabela de palavras reservadas. enquanto recebe significado especial;contador continua sendo um nome escolhido pela pessoa.

08 / TER PEÇAS NÃO SIGNIFICA TER UMA FRASE

O lexer aceita “var = 10”; o parser precisa recusá-lo.

Todos os caracteres dessa frase existem no vocabulário. Mesmo assim, falta um nome entre var e =. O lexer não deveria conhecer essa regra. Quem valida a organização é o parser, ou analisador sintático.

Sintaxe é o conjunto de formas permitidas. Gramática é uma maneira precisa de descrever essas formas. Nossa declaração de variável pode ser lida assim:

declaração  → "var" IDENTIFICADOR "=" expressão ";"

exemplo válido:
var pontos = 10 + bonus;
“VAR”comece a declaraçãopalavra obrigatória
NOMEpontosidentificador obrigatório
“=”receberáoperador obrigatório
EXPRESSÃO10 + bonusestrutura recursiva
“;”encerredelimitador obrigatório
fn variable_declaration(&mut self) -> Result<Stmt> {
    let name = self.identifier("esperava o nome da variável")?;
    self.consume(is_equal, "esperava '='")?;
    let initializer = self.expression()?;
    self.semicolon()?;
    Ok(Stmt::Var { name, initializer })
}

consume significa “o próximo token precisa ter esta forma”. Se tiver, avançamos. Se não tiver, criamos um erro sintático na posição do token. A gramática deixou de ser um texto e virou controle de fluxo em Rust.

09 / O PARSER PRECISA ENTREGAR MAIS QUE “VÁLIDO”

A saída do parser é um mapa estrutural do programa.

var resultado = 2 + 3 * 4;

Não basta responder “a frase está correta”. O interpretador precisará saber que existe uma declaração chamada resultado, cujo valor é uma soma, e que o lado direito dessa soma é uma multiplicação.

Essa estrutura é a AST: Abstract Syntax Tree, ou árvore de sintaxe abstrata. É uma árvore porque cada nó pode apontar para filhos. É “abstrata” porque remove detalhes que já cumpriram sua função, como parênteses e ponto e vírgula, preservando relações importantes.

pub enum Expr {
    Number(f64),
    Variable(String),
    Binary {
        left: Box<Expr>,
        operator: BinaryOp,
        right: Box<Expr>,
    },
    Call {
        name: String,
        arguments: Vec<Expr>,
    },
}

é apenas um elemento da árvore. UmExpr::Binary é um nó com dois filhos.Box<Expr> guarda o filho indiretamente, permitindo que o enum recursivo tenha tamanho conhecido em Rust.

10 / QUAL OPERAÇÃO ACONTECE PRIMEIRO?

Sem precedência, 2 + 3 × 4 poderia produzir 20 ou 14.

Pessoas aprendem que multiplicação vem antes de soma. O computador não recebe essa regra da matemática por telepatia. Nossa gramática precisa codificá-la.

CamadaOperadoresChama a camada mais forte
ououe
eeigualdade
igualdade== !=comparação
comparação> >= < <=termo
termo+ -fator
fator* /unário

term() pede seus operandos a factor(). Portanto a multiplicação é agrupada antes de a soma construir seu nó. Parênteses chamam novamente expression() e permitem que a pessoa escolha outro agrupamento.

2 + 3 * 42 + (3 × 4)resultado 14
(2 + 3) * 4(2 + 3) × 4resultado 20

11 / A ÁRVORE ESTÁ PRONTA; AGORA ELA PRECISA VIVER

Runtime é tudo o que existe enquanto o programa está executando.

O parser já foi embora quando somamos dois números. A AST descreve a operação, mas não contém uma CPU imaginária. O interpretador percorre os nós, e o runtime fornece valores, memória de variáveis, escopos e fluxo de funções.

pub enum Value {
    Number(f64),
    String(String),
    Bool(bool),
    Function(FunctionDecl),
    Nil,
}

Value é uma caixa que pode carregar qualquer valor da Pulso.Number(42.0) e String("42") parecem parecidos ao imprimir, mas obedecem a regras diferentes. Somar números calcula; somar textos concatena; misturar os dois produz um erro claro.

Binary(+)o que fazer
FILHOSavaliar esquerda e direitaobter valores
REGRAnúmero + númerovalidar tipos
RESULTADOValue::Numberdevolver à árvore

12 / ONDE UMA VARIÁVEL MORA?

Um nome não guarda o valor dentro das letras.

var nome = "fora";
{
    var nome = "dentro";
    mostre(nome);
}
mostre(nome);

A primeira variável pertence ao escopo global. O bloco abre um escopo interno, no qual outro nome pode existir. Ao procurar um nome, começamos no escopo mais próximo e caminhamos para fora. Ao terminar o bloco, removemos apenas a camada interna.

ESCOPO GLOBALnome → “fora”continua vivo até o fim
sobre ele
ESCOPO DO BLOCOnome → “dentro”vence durante o bloco
pub struct Environment {
    scopes: Vec<BTreeMap<String, Value>>,
}

pub fn get(&self, name: &str) -> Result<Value> {
    for scope in self.scopes.iter().rev() {
        if let Some(value) = scope.get(name) {
            return Ok(value.clone());
        }
    }
    Err(/* nome não declarado */)
}

BTreeMap associa cada texto a um valor. O vetor representa uma pilha de mapas. iter().rev() começa pela camada mais recente. Isso ésombreamento: um nome interno esconde temporariamente outro nome igual, sem destruí-lo.

13 / PROGRAMAS PRECISAM ESCOLHER E REPETIR

Primeiro escolhemos um caminho. Depois aprendemos a percorrê-lo novamente.

Até aqui, a Pulso executa todas as instruções uma vez, de cima para baixo. Mas programas úteis precisam reagir: mostrar uma mensagem somente quando uma senha estiver correta, ou repetir uma tentativa enquanto ainda houver vidas. Chamamos essa mudança de caminho de controle de fluxo.

se avalia uma pergunta e escolhe um ramo.enquanto avalia uma pergunta, executa um bloco e volta para perguntar novamente. Na Pulso, falso e nulo contam como falsos; os demais valores contam como verdadeiros.

Stmt::If {
    condition,
    then_branch,
    else_branch,
} => {
    if self.evaluate(condition)?.is_truthy() {
        self.execute(then_branch)
    } else if let Some(other) = else_branch {
        self.execute(other)
    } else {
        Ok(Flow::Continue)
    }
}
CONDIÇÃOcontador < 3?
verdadeiroexecutar corpovoltar à condição
falsosair do laçocontinuar o programa

while não precisa de uma máquina especial. O interpretador avalia a condição; se for verdadeira, executa o corpo e volta. O cuidado fica na linguagem escrita pelo usuário: se nada tornar a condição falsa, o laço será infinito.

14 / COMO EVITAR REPETIR UMA RECEITA?

Função é uma receita com nome que recebe ingredientes e pode devolver um resultado.

funcao area(largura, altura) {
    var resultado = largura * altura;
    retorne resultado;
}

mostre(area(6, 7));

Poderíamos copiar a multiplicação toda vez que precisássemos calcular uma área. Isso espalharia a mesma regra pelo programa. Uma função reúne essa regra uma vez: area é o nome da receita; largura ealtura são os ingredientes, chamados de parâmetros.

Ao declarar a função, não executamos o corpo. Guardamos umFunctionDecl no ambiente. Ao chamar, avaliamos os argumentos, criamos um novo escopo, associamos cada parâmetro ao valor recebido e então executamos o corpo.

CHAMADAarea(6, 7)avaliar argumentos
NOVO ESCOPOlargura=6 · altura=7parâmetros locais
CORPOresultado=42executar comandos
RETORNO42voltar à chamada

Return não é apenas um valor

retorne precisa interromper o restante do corpo, atravessar blocos e laços e devolver um valor à chamada. Por isso o runtime usa dois fluxos:

pub enum Flow {
    Continue,
    Return(Value),
}

Continue diz “execute o próximo comando”.Return(value) diz “pare esta função e transporte o valor para quem chamou”. Essa pequena enumeração evita confundir retorno com um erro.

15 / “NÃO FUNCIONOU” É UMA MENSAGEM RUIM

Cada etapa conhece uma família diferente de problemas.

EtapaExemploO que sabemos
léxicavar preço = @;o caractere não pertence ao vocabulário
sintáticavar = 10;os tokens não seguem a gramática
runtime/semântica10 + "oi"a estrutura é válida, mas a operação não faz sentido

“Semântico” diz respeito ao significado. A Pulso descobre parte desses problemas durante a execução, por isso os chama de erros de runtime. Uma linguagem com verificação estática poderia encontrar vários deles antes de rodar.

erro de léxico na linha 1, coluna 13:
caractere inesperado: '@'

erro de sintático na linha 1, coluna 5:
esperava o nome da variável

erro de runtime:
'+' não combina número com texto

Guardar linha e coluna em cada token cria uma trilha. O parser não precisa recontar caracteres: quando falta um ponto e vírgula, ele consulta a posição do token atual e explica onde a expectativa foi quebrada.

16 / INTERPRETAR NÃO É O ÚNICO FINAL POSSÍVEL

O próximo artigo não jogará nossa linguagem fora.

Lexer, tokens, parser e AST respondem “o que o programa significa?”. Depois dessa resposta, podemos escolher como fazê-lo executar. Neste artigo, o interpretador percorre a AST diretamente. No próximo artigo, um compilador transformará a mesma árvore em outra representação.

FRONT-END COMPARTILHADOfonte → tokens → ASTa linguagem continua sendo Pulso
CAMINHO Ainterpretadorvisitar a AST agora
CAMINHO Bcompiladorproduzir bytecode, assembly ou executável

Front-end é a parte que entende a linguagem de entrada.Back-end, neste contexto, é a parte que escolhe a forma de execução ou o código de saída. Não tem relação com front-end e back-end web.

CONTINUIDADE REAL

No artigo do compilador, trocaremos a última seta. A sintaxe, o lexer, o parser e a AST da Pulso continuarão conosco.

17 / A OFICINA COMPLETA

Cada módulo responde a uma pergunta que surgiu na narrativa.

pulso/
├── Cargo.toml
├── README.md
├── examples/
│   └── boas-vindas.pulso
├── src/
│   ├── lib.rs
│   ├── main.rs
│   ├── token.rs
│   ├── lexer.rs
│   ├── ast.rs
│   ├── parser.rs
│   ├── value.rs
│   ├── environment.rs
│   ├── runtime.rs
│   ├── interpreter.rs
│   └── errors.rs
└── tests/
    └── language.rs
Baixar o projeto completo da Pulso
ENTRADAlexer · tokentexto vira peças
FORMAparser · astpeças viram árvore
ESTADOvalue · environmentvalores ganham nomes
AÇÃOruntime · interpreterárvore vira comportamento

Os painéis abaixo disponibilizam todo o código. A ordem de leitura acompanha o pipeline: comece por token e lexer, siga para AST e parser, depois leia valores, ambiente, runtime e interpretador.

01
Cargo.toml

Declara a biblioteca e o executável Pulso

Ver código completo de Cargo.toml
[package]
name = "pulso"
version = "0.1.0"
edition = "2021"
description = "Linguagem didática construída do zero em Rust"

[lib]
name = "pulso"
path = "src/lib.rs"

[[bin]]
name = "pulso"
path = "src/main.rs"

[dependencies]
02
src/lib.rs

Conecta o pipeline inteiro em quatro linhas

Ver código completo de src/lib.rs
pub mod ast;
pub mod environment;
pub mod errors;
pub mod interpreter;
pub mod lexer;
pub mod parser;
pub mod runtime;
pub mod token;
pub mod value;

use errors::Result;
use interpreter::Interpreter;
use lexer::Lexer;
use parser::Parser;

pub fn run(source: &str) -> Result<Vec<String>> {
    let tokens = Lexer::new(source).scan()?;
    let program = Parser::new(tokens).parse()?;
    Interpreter::new().run(&program)
}
03
src/token.rs

Define o vocabulário reconhecido pela linguagem

Ver código completo de src/token.rs
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
    LeftParen,
    RightParen,
    LeftBrace,
    RightBrace,
    Comma,
    Semicolon,
    Plus,
    Minus,
    Star,
    Slash,
    Bang,
    BangEqual,
    Equal,
    EqualEqual,
    Greater,
    GreaterEqual,
    Less,
    LessEqual,
    Identifier(String),
    Number(f64),
    String(String),
    Var,
    Print,
    If,
    Else,
    While,
    Function,
    Return,
    True,
    False,
    And,
    Or,
    Eof,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Token {
    pub kind: TokenKind,
    pub line: usize,
    pub column: usize,
}

impl Token {
    pub fn new(kind: TokenKind, line: usize, column: usize) -> Self {
        Self { kind, line, column }
    }
}
04
src/lexer.rs

Transforma caracteres em tokens com posição

Ver código completo de src/lexer.rs
use crate::{
    errors::{LangError, Result},
    token::{Token, TokenKind},
};

pub struct Lexer {
    chars: Vec<char>,
    current: usize,
    line: usize,
    column: usize,
}

impl Lexer {
    pub fn new(source: &str) -> Self {
        Self { chars: source.chars().collect(), current: 0, line: 1, column: 1 }
    }

    pub fn scan(mut self) -> Result<Vec<Token>> {
        let mut tokens = Vec::new();
        while !self.is_at_end() {
            let line = self.line;
            let column = self.column;
            let character = self.advance();
            let kind = match character {
                '(' => Some(TokenKind::LeftParen),
                ')' => Some(TokenKind::RightParen),
                '{' => Some(TokenKind::LeftBrace),
                '}' => Some(TokenKind::RightBrace),
                ',' => Some(TokenKind::Comma),
                ';' => Some(TokenKind::Semicolon),
                '+' => Some(TokenKind::Plus),
                '-' => Some(TokenKind::Minus),
                '*' => Some(TokenKind::Star),
                '/' if self.peek() == Some('/') => {
                    while self.peek().is_some_and(|next| next != '\n') {
                        self.advance();
                    }
                    None
                }
                '/' => Some(TokenKind::Slash),
                '!' => Some(if self.consume('=') {
                    TokenKind::BangEqual
                } else {
                    TokenKind::Bang
                }),
                '=' => Some(if self.consume('=') {
                    TokenKind::EqualEqual
                } else {
                    TokenKind::Equal
                }),
                '>' => Some(if self.consume('=') {
                    TokenKind::GreaterEqual
                } else {
                    TokenKind::Greater
                }),
                '<' => Some(if self.consume('=') {
                    TokenKind::LessEqual
                } else {
                    TokenKind::Less
                }),
                '"' => Some(self.string(line, column)?),
                character if character.is_ascii_digit() => {
                    Some(self.number(character, line, column)?)
                }
                character if is_identifier_start(character) => {
                    Some(self.identifier(character))
                }
                character if character.is_whitespace() => None,
                other => {
                    return Err(LangError::new(
                        "léxico",
                        line,
                        column,
                        format!("caractere inesperado: {other:?}"),
                    ));
                }
            };
            if let Some(kind) = kind {
                tokens.push(Token::new(kind, line, column));
            }
        }
        tokens.push(Token::new(TokenKind::Eof, self.line, self.column));
        Ok(tokens)
    }

    fn string(&mut self, line: usize, column: usize) -> Result<TokenKind> {
        let mut value = String::new();
        while let Some(character) = self.peek() {
            if character == '"' {
                self.advance();
                return Ok(TokenKind::String(value));
            }
            if character == '\\' {
                self.advance();
                let escaped = self.peek().ok_or_else(|| {
                    LangError::new(
                        "léxico",
                        self.line,
                        self.column,
                        "texto terminou depois de uma barra invertida",
                    )
                })?;
                self.advance();
                value.push(match escaped {
                    'n' => '\n',
                    't' => '\t',
                    '"' => '"',
                    '\\' => '\\',
                    other => {
                        return Err(LangError::new(
                            "léxico",
                            self.line,
                            self.column,
                            format!("escape desconhecido: \\{other}"),
                        ));
                    }
                });
            } else {
                value.push(self.advance());
            }
        }
        Err(LangError::new("léxico", line, column, "texto sem aspas de fechamento"))
    }

    fn number(&mut self, first: char, line: usize, column: usize) -> Result<TokenKind> {
        let mut text = first.to_string();
        while self.peek().is_some_and(|character| character.is_ascii_digit()) {
            text.push(self.advance());
        }
        if self.peek() == Some('.')
            && self.peek_next().is_some_and(|character| character.is_ascii_digit())
        {
            text.push(self.advance());
            while self.peek().is_some_and(|character| character.is_ascii_digit()) {
                text.push(self.advance());
            }
        }
        let number = text.parse().map_err(|_| {
            LangError::new("léxico", line, column, format!("número inválido: {text}"))
        })?;
        Ok(TokenKind::Number(number))
    }

    fn identifier(&mut self, first: char) -> TokenKind {
        let mut text = first.to_string();
        while self.peek().is_some_and(is_identifier_continue) {
            text.push(self.advance());
        }
        match text.as_str() {
            "var" => TokenKind::Var,
            "mostre" => TokenKind::Print,
            "se" => TokenKind::If,
            "senao" => TokenKind::Else,
            "enquanto" => TokenKind::While,
            "funcao" => TokenKind::Function,
            "retorne" => TokenKind::Return,
            "verdadeiro" => TokenKind::True,
            "falso" => TokenKind::False,
            "e" => TokenKind::And,
            "ou" => TokenKind::Or,
            _ => TokenKind::Identifier(text),
        }
    }

    fn advance(&mut self) -> char {
        let character = self.chars[self.current];
        self.current += 1;
        if character == '\n' {
            self.line += 1;
            self.column = 1;
        } else {
            self.column += 1;
        }
        character
    }

    fn consume(&mut self, expected: char) -> bool {
        if self.peek() == Some(expected) {
            self.advance();
            true
        } else {
            false
        }
    }

    fn peek(&self) -> Option<char> {
        self.chars.get(self.current).copied()
    }

    fn peek_next(&self) -> Option<char> {
        self.chars.get(self.current + 1).copied()
    }

    fn is_at_end(&self) -> bool {
        self.current >= self.chars.len()
    }
}

fn is_identifier_start(character: char) -> bool {
    character == '_' || character.is_alphabetic()
}

fn is_identifier_continue(character: char) -> bool {
    character == '_' || character.is_alphanumeric()
}
05
src/ast.rs

Modela expressões, comandos e funções como árvore

Ver código completo de src/ast.rs
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
    pub statements: Vec<Stmt>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
    Var { name: String, initializer: Expr },
    Assign { name: String, value: Expr },
    Print(Expr),
    Expression(Expr),
    Block(Vec<Stmt>),
    If { condition: Expr, then_branch: Box<Stmt>, else_branch: Option<Box<Stmt>> },
    While { condition: Expr, body: Box<Stmt> },
    Function(FunctionDecl),
    Return(Option<Expr>),
}

#[derive(Debug, Clone, PartialEq)]
pub struct FunctionDecl {
    pub name: String,
    pub parameters: Vec<String>,
    pub body: Vec<Stmt>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
    Number(f64),
    String(String),
    Bool(bool),
    Variable(String),
    Unary { operator: UnaryOp, right: Box<Expr> },
    Binary { left: Box<Expr>, operator: BinaryOp, right: Box<Expr> },
    Call { name: String, arguments: Vec<Expr> },
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UnaryOp {
    Negate,
    Not,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BinaryOp {
    Add,
    Subtract,
    Multiply,
    Divide,
    Equal,
    NotEqual,
    Greater,
    GreaterEqual,
    Less,
    LessEqual,
    And,
    Or,
}
06
src/parser.rs

Aplica a gramática e constrói a AST

Ver código completo de src/parser.rs
use crate::{
    ast::{BinaryOp, Expr, FunctionDecl, Program, Stmt, UnaryOp},
    errors::{LangError, Result},
    token::{Token, TokenKind},
};

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

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

    pub fn parse(mut self) -> Result<Program> {
        let mut statements = Vec::new();
        while !self.is_at_end() {
            statements.push(self.declaration()?);
        }
        Ok(Program { statements })
    }

    fn declaration(&mut self) -> Result<Stmt> {
        if self.matches(|kind| matches!(kind, TokenKind::Function)) {
            return self.function();
        }
        if self.matches(|kind| matches!(kind, TokenKind::Var)) {
            return self.variable_declaration();
        }
        self.statement()
    }

    fn function(&mut self) -> Result<Stmt> {
        let name = self.identifier("esperava o nome da função")?;
        self.consume(|kind| matches!(kind, TokenKind::LeftParen), "esperava '('")?;
        let mut parameters = Vec::new();
        if !self.check(|kind| matches!(kind, TokenKind::RightParen)) {
            loop {
                if parameters.len() == 32 {
                    return Err(self.error("uma função aceita no máximo 32 parâmetros"));
                }
                parameters.push(self.identifier("esperava o nome do parâmetro")?);
                if !self.matches(|kind| matches!(kind, TokenKind::Comma)) {
                    break;
                }
            }
        }
        self.consume(|kind| matches!(kind, TokenKind::RightParen), "esperava ')'")?;
        self.consume(|kind| matches!(kind, TokenKind::LeftBrace), "esperava '{'")?;
        let body = self.block_items()?;
        Ok(Stmt::Function(FunctionDecl { name, parameters, body }))
    }

    fn variable_declaration(&mut self) -> Result<Stmt> {
        let name = self.identifier("esperava o nome da variável")?;
        self.consume(|kind| matches!(kind, TokenKind::Equal), "esperava '='")?;
        let initializer = self.expression()?;
        self.semicolon()?;
        Ok(Stmt::Var { name, initializer })
    }

    fn statement(&mut self) -> Result<Stmt> {
        if self.matches(|kind| matches!(kind, TokenKind::Print)) {
            return self.print_statement();
        }
        if self.matches(|kind| matches!(kind, TokenKind::If)) {
            return self.if_statement();
        }
        if self.matches(|kind| matches!(kind, TokenKind::While)) {
            return self.while_statement();
        }
        if self.matches(|kind| matches!(kind, TokenKind::Return)) {
            return self.return_statement();
        }
        if self.matches(|kind| matches!(kind, TokenKind::LeftBrace)) {
            return Ok(Stmt::Block(self.block_items()?));
        }
        if let TokenKind::Identifier(name) = self.peek().kind.clone() {
            if self.peek_next().is_some_and(|token| matches!(&token.kind, TokenKind::Equal)) {
                self.advance();
                self.advance();
                let value = self.expression()?;
                self.semicolon()?;
                return Ok(Stmt::Assign { name, value });
            }
        }
        let expression = self.expression()?;
        self.semicolon()?;
        Ok(Stmt::Expression(expression))
    }

    fn print_statement(&mut self) -> Result<Stmt> {
        self.consume(|kind| matches!(kind, TokenKind::LeftParen), "esperava '('")?;
        let expression = self.expression()?;
        self.consume(|kind| matches!(kind, TokenKind::RightParen), "esperava ')'")?;
        self.semicolon()?;
        Ok(Stmt::Print(expression))
    }

    fn if_statement(&mut self) -> Result<Stmt> {
        self.consume(|kind| matches!(kind, TokenKind::LeftParen), "esperava '('")?;
        let condition = self.expression()?;
        self.consume(|kind| matches!(kind, TokenKind::RightParen), "esperava ')'")?;
        let then_branch = Box::new(self.statement()?);
        let else_branch = if self.matches(|kind| matches!(kind, TokenKind::Else)) {
            Some(Box::new(self.statement()?))
        } else {
            None
        };
        Ok(Stmt::If { condition, then_branch, else_branch })
    }

    fn while_statement(&mut self) -> Result<Stmt> {
        self.consume(|kind| matches!(kind, TokenKind::LeftParen), "esperava '('")?;
        let condition = self.expression()?;
        self.consume(|kind| matches!(kind, TokenKind::RightParen), "esperava ')'")?;
        let body = Box::new(self.statement()?);
        Ok(Stmt::While { condition, body })
    }

    fn return_statement(&mut self) -> Result<Stmt> {
        let value = if self.check(|kind| matches!(kind, TokenKind::Semicolon)) {
            None
        } else {
            Some(self.expression()?)
        };
        self.semicolon()?;
        Ok(Stmt::Return(value))
    }

    fn block_items(&mut self) -> Result<Vec<Stmt>> {
        let mut statements = Vec::new();
        while !self.check(|kind| matches!(kind, TokenKind::RightBrace)) && !self.is_at_end() {
            statements.push(self.declaration()?);
        }
        self.consume(|kind| matches!(kind, TokenKind::RightBrace), "esperava '}'")?;
        Ok(statements)
    }

    fn expression(&mut self) -> Result<Expr> {
        self.or()
    }

    fn or(&mut self) -> Result<Expr> {
        let mut expression = self.and()?;
        while self.matches(|kind| matches!(kind, TokenKind::Or)) {
            let right = self.and()?;
            expression = binary(expression, BinaryOp::Or, right);
        }
        Ok(expression)
    }

    fn and(&mut self) -> Result<Expr> {
        let mut expression = self.equality()?;
        while self.matches(|kind| matches!(kind, TokenKind::And)) {
            let right = self.equality()?;
            expression = binary(expression, BinaryOp::And, right);
        }
        Ok(expression)
    }

    fn equality(&mut self) -> Result<Expr> {
        let mut expression = self.comparison()?;
        loop {
            let operator = if self.matches(|kind| matches!(kind, TokenKind::EqualEqual)) {
                Some(BinaryOp::Equal)
            } else if self.matches(|kind| matches!(kind, TokenKind::BangEqual)) {
                Some(BinaryOp::NotEqual)
            } else {
                None
            };
            let Some(operator) = operator else { break };
            expression = binary(expression, operator, self.comparison()?);
        }
        Ok(expression)
    }

    fn comparison(&mut self) -> Result<Expr> {
        let mut expression = self.term()?;
        loop {
            let operator = if self.matches(|kind| matches!(kind, TokenKind::Greater)) {
                Some(BinaryOp::Greater)
            } else if self.matches(|kind| matches!(kind, TokenKind::GreaterEqual)) {
                Some(BinaryOp::GreaterEqual)
            } else if self.matches(|kind| matches!(kind, TokenKind::Less)) {
                Some(BinaryOp::Less)
            } else if self.matches(|kind| matches!(kind, TokenKind::LessEqual)) {
                Some(BinaryOp::LessEqual)
            } else {
                None
            };
            let Some(operator) = operator else { break };
            expression = binary(expression, operator, self.term()?);
        }
        Ok(expression)
    }

    fn term(&mut self) -> Result<Expr> {
        let mut expression = self.factor()?;
        loop {
            let operator = if self.matches(|kind| matches!(kind, TokenKind::Plus)) {
                Some(BinaryOp::Add)
            } else if self.matches(|kind| matches!(kind, TokenKind::Minus)) {
                Some(BinaryOp::Subtract)
            } else {
                None
            };
            let Some(operator) = operator else { break };
            expression = binary(expression, operator, self.factor()?);
        }
        Ok(expression)
    }

    fn factor(&mut self) -> Result<Expr> {
        let mut expression = self.unary()?;
        loop {
            let operator = if self.matches(|kind| matches!(kind, TokenKind::Star)) {
                Some(BinaryOp::Multiply)
            } else if self.matches(|kind| matches!(kind, TokenKind::Slash)) {
                Some(BinaryOp::Divide)
            } else {
                None
            };
            let Some(operator) = operator else { break };
            expression = binary(expression, operator, self.unary()?);
        }
        Ok(expression)
    }

    fn unary(&mut self) -> Result<Expr> {
        if self.matches(|kind| matches!(kind, TokenKind::Bang)) {
            return Ok(Expr::Unary { operator: UnaryOp::Not, right: Box::new(self.unary()?) });
        }
        if self.matches(|kind| matches!(kind, TokenKind::Minus)) {
            return Ok(Expr::Unary {
                operator: UnaryOp::Negate,
                right: Box::new(self.unary()?),
            });
        }
        self.call()
    }

    fn call(&mut self) -> Result<Expr> {
        let mut expression = self.primary()?;
        while self.matches(|kind| matches!(kind, TokenKind::LeftParen)) {
            let name = match expression {
                Expr::Variable(name) => name,
                _ => return Err(self.error("somente funções com nome podem ser chamadas")),
            };
            let mut arguments = Vec::new();
            if !self.check(|kind| matches!(kind, TokenKind::RightParen)) {
                loop {
                    arguments.push(self.expression()?);
                    if !self.matches(|kind| matches!(kind, TokenKind::Comma)) {
                        break;
                    }
                }
            }
            self.consume(|kind| matches!(kind, TokenKind::RightParen), "esperava ')'")?;
            expression = Expr::Call { name, arguments };
        }
        Ok(expression)
    }

    fn primary(&mut self) -> Result<Expr> {
        let token = self.advance().clone();
        match token.kind {
            TokenKind::Number(value) => Ok(Expr::Number(value)),
            TokenKind::String(value) => Ok(Expr::String(value)),
            TokenKind::True => Ok(Expr::Bool(true)),
            TokenKind::False => Ok(Expr::Bool(false)),
            TokenKind::Identifier(name) => Ok(Expr::Variable(name)),
            TokenKind::LeftParen => {
                let expression = self.expression()?;
                self.consume(|kind| matches!(kind, TokenKind::RightParen), "esperava ')'")?;
                Ok(expression)
            }
            _ => Err(LangError::new(
                "sintático",
                token.line,
                token.column,
                "esperava um número, texto, variável ou expressão entre parênteses",
            )),
        }
    }

    fn semicolon(&mut self) -> Result<()> {
        self.consume(|kind| matches!(kind, TokenKind::Semicolon), "esperava ';'")?;
        Ok(())
    }

    fn identifier(&mut self, message: &str) -> Result<String> {
        let token = self.advance().clone();
        if let TokenKind::Identifier(name) = token.kind {
            Ok(name)
        } else {
            Err(LangError::new("sintático", token.line, token.column, message))
        }
    }

    fn consume(
        &mut self,
        predicate: impl Fn(&TokenKind) -> bool,
        message: &str,
    ) -> Result<&Token> {
        if self.check(predicate) {
            Ok(self.advance())
        } else {
            Err(self.error(message))
        }
    }

    fn matches(&mut self, predicate: impl Fn(&TokenKind) -> bool) -> bool {
        if self.check(predicate) {
            self.advance();
            true
        } else {
            false
        }
    }

    fn check(&self, predicate: impl Fn(&TokenKind) -> bool) -> bool {
        predicate(&self.peek().kind)
    }

    fn advance(&mut self) -> &Token {
        if !self.is_at_end() {
            self.current += 1;
        }
        &self.tokens[self.current - 1]
    }

    fn peek(&self) -> &Token {
        &self.tokens[self.current]
    }

    fn peek_next(&self) -> Option<&Token> {
        self.tokens.get(self.current + 1)
    }

    fn is_at_end(&self) -> bool {
        matches!(&self.peek().kind, TokenKind::Eof)
    }

    fn error(&self, message: &str) -> LangError {
        let token = self.peek();
        LangError::new("sintático", token.line, token.column, message)
    }
}

fn binary(left: Expr, operator: BinaryOp, right: Expr) -> Expr {
    Expr::Binary { left: Box::new(left), operator, right: Box::new(right) }
}
07
src/value.rs

Representa os valores existentes durante a execução

Ver código completo de src/value.rs
use crate::ast::FunctionDecl;
use std::fmt;

#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    Number(f64),
    String(String),
    Bool(bool),
    Function(FunctionDecl),
    Nil,
}

impl Value {
    pub fn is_truthy(&self) -> bool {
        match self {
            Self::Bool(value) => *value,
            Self::Nil => false,
            _ => true,
        }
    }

    pub fn type_name(&self) -> &'static str {
        match self {
            Self::Number(_) => "número",
            Self::String(_) => "texto",
            Self::Bool(_) => "booleano",
            Self::Function(_) => "função",
            Self::Nil => "nulo",
        }
    }
}

impl fmt::Display for Value {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Number(value) if value.fract() == 0.0 => write!(formatter, "{value:.0}"),
            Self::Number(value) => write!(formatter, "{value}"),
            Self::String(value) => write!(formatter, "{value}"),
            Self::Bool(value) => write!(formatter, "{value}"),
            Self::Function(function) => write!(formatter, "<função {}>", function.name),
            Self::Nil => write!(formatter, "nulo"),
        }
    }
}
08
src/environment.rs

Guarda variáveis em escopos aninhados

Ver código completo de src/environment.rs
use crate::{
    errors::{LangError, Result},
    value::Value,
};
use std::collections::BTreeMap;

#[derive(Debug, Clone)]
pub struct Environment {
    scopes: Vec<BTreeMap<String, Value>>,
}

impl Environment {
    pub fn new() -> Self {
        Self { scopes: vec![BTreeMap::new()] }
    }

    pub fn push_scope(&mut self) {
        self.scopes.push(BTreeMap::new());
    }

    pub fn pop_scope(&mut self) {
        if self.scopes.len() > 1 {
            self.scopes.pop();
        }
    }

    pub fn define(&mut self, name: impl Into<String>, value: Value) -> Result<()> {
        let name = name.into();
        let scope = self.scopes.last_mut().expect("sempre existe um escopo global");
        if scope.contains_key(&name) {
            return Err(LangError::runtime(format!(
                "a variável '{name}' já existe neste escopo"
            )));
        }
        scope.insert(name, value);
        Ok(())
    }

    pub fn assign(&mut self, name: &str, value: Value) -> Result<()> {
        for scope in self.scopes.iter_mut().rev() {
            if scope.contains_key(name) {
                scope.insert(name.to_string(), value);
                return Ok(());
            }
        }
        Err(LangError::runtime(format!("variável '{name}' não foi declarada")))
    }

    pub fn get(&self, name: &str) -> Result<Value> {
        for scope in self.scopes.iter().rev() {
            if let Some(value) = scope.get(name) {
                return Ok(value.clone());
            }
        }
        Err(LangError::runtime(format!("nome '{name}' não foi declarado")))
    }
}

impl Default for Environment {
    fn default() -> Self {
        Self::new()
    }
}
09
src/runtime.rs

Transporta o fluxo normal ou um retorno de função

Ver código completo de src/runtime.rs
use crate::value::Value;

#[derive(Debug, Clone, PartialEq)]
pub enum Flow {
    Continue,
    Return(Value),
}
10
src/interpreter.rs

Percorre a AST e produz comportamento

Ver código completo de src/interpreter.rs
use crate::{
    ast::{BinaryOp, Expr, Program, Stmt, UnaryOp},
    environment::Environment,
    errors::{LangError, Result},
    runtime::Flow,
    value::Value,
};

pub struct Interpreter {
    environment: Environment,
    output: Vec<String>,
}

impl Interpreter {
    pub fn new() -> Self {
        Self { environment: Environment::new(), output: Vec::new() }
    }

    pub fn run(mut self, program: &Program) -> Result<Vec<String>> {
        for statement in &program.statements {
            if let Flow::Return(_) = self.execute(statement)? {
                return Err(LangError::runtime("'retorne' só pode aparecer dentro de uma função"));
            }
        }
        Ok(self.output)
    }

    fn execute(&mut self, statement: &Stmt) -> Result<Flow> {
        match statement {
            Stmt::Var { name, initializer } => {
                let value = self.evaluate(initializer)?;
                self.environment.define(name.clone(), value)?;
                Ok(Flow::Continue)
            }
            Stmt::Assign { name, value } => {
                let value = self.evaluate(value)?;
                self.environment.assign(name, value)?;
                Ok(Flow::Continue)
            }
            Stmt::Print(expression) => {
                let value = self.evaluate(expression)?;
                self.output.push(value.to_string());
                Ok(Flow::Continue)
            }
            Stmt::Expression(expression) => {
                self.evaluate(expression)?;
                Ok(Flow::Continue)
            }
            Stmt::Block(statements) => self.execute_block(statements),
            Stmt::If { condition, then_branch, else_branch } => {
                if self.evaluate(condition)?.is_truthy() {
                    self.execute(then_branch)
                } else if let Some(else_branch) = else_branch {
                    self.execute(else_branch)
                } else {
                    Ok(Flow::Continue)
                }
            }
            Stmt::While { condition, body } => {
                while self.evaluate(condition)?.is_truthy() {
                    if let Flow::Return(value) = self.execute(body)? {
                        return Ok(Flow::Return(value));
                    }
                }
                Ok(Flow::Continue)
            }
            Stmt::Function(function) => {
                self.environment
                    .define(function.name.clone(), Value::Function(function.clone()))?;
                Ok(Flow::Continue)
            }
            Stmt::Return(expression) => {
                let value = match expression {
                    Some(expression) => self.evaluate(expression)?,
                    None => Value::Nil,
                };
                Ok(Flow::Return(value))
            }
        }
    }

    fn execute_block(&mut self, statements: &[Stmt]) -> Result<Flow> {
        self.environment.push_scope();
        let result = (|| {
            for statement in statements {
                if let Flow::Return(value) = self.execute(statement)? {
                    return Ok(Flow::Return(value));
                }
            }
            Ok(Flow::Continue)
        })();
        self.environment.pop_scope();
        result
    }

    fn evaluate(&mut self, expression: &Expr) -> Result<Value> {
        match expression {
            Expr::Number(value) => Ok(Value::Number(*value)),
            Expr::String(value) => Ok(Value::String(value.clone())),
            Expr::Bool(value) => Ok(Value::Bool(*value)),
            Expr::Variable(name) => self.environment.get(name),
            Expr::Unary { operator, right } => {
                let right = self.evaluate(right)?;
                match operator {
                    UnaryOp::Not => Ok(Value::Bool(!right.is_truthy())),
                    UnaryOp::Negate => match right {
                        Value::Number(number) => Ok(Value::Number(-number)),
                        other => Err(type_error("o operador '-' exige número", &other)),
                    },
                }
            }
            Expr::Binary { left, operator, right } => {
                if *operator == BinaryOp::And {
                    let left = self.evaluate(left)?;
                    return if left.is_truthy() {
                        self.evaluate(right)
                    } else {
                        Ok(left)
                    };
                }
                if *operator == BinaryOp::Or {
                    let left = self.evaluate(left)?;
                    return if left.is_truthy() {
                        Ok(left)
                    } else {
                        self.evaluate(right)
                    };
                }
                let left = self.evaluate(left)?;
                let right = self.evaluate(right)?;
                self.binary(left, *operator, right)
            }
            Expr::Call { name, arguments } => self.call(name, arguments),
        }
    }

    fn binary(&self, left: Value, operator: BinaryOp, right: Value) -> Result<Value> {
        match operator {
            BinaryOp::Add => match (left, right) {
                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
                (Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
                (a, b) => Err(LangError::runtime(format!(
                    "'+' não combina {} com {}",
                    a.type_name(),
                    b.type_name()
                ))),
            },
            BinaryOp::Subtract => numbers(left, right, |a, b| a - b),
            BinaryOp::Multiply => numbers(left, right, |a, b| a * b),
            BinaryOp::Divide => {
                if right == Value::Number(0.0) {
                    return Err(LangError::runtime("divisão por zero"));
                }
                numbers(left, right, |a, b| a / b)
            }
            BinaryOp::Equal => Ok(Value::Bool(left == right)),
            BinaryOp::NotEqual => Ok(Value::Bool(left != right)),
            BinaryOp::Greater => compare(left, right, |a, b| a > b),
            BinaryOp::GreaterEqual => compare(left, right, |a, b| a >= b),
            BinaryOp::Less => compare(left, right, |a, b| a < b),
            BinaryOp::LessEqual => compare(left, right, |a, b| a <= b),
            BinaryOp::And | BinaryOp::Or => unreachable!("tratados com curto-circuito"),
        }
    }

    fn call(&mut self, name: &str, arguments: &[Expr]) -> Result<Value> {
        let function = match self.environment.get(name)? {
            Value::Function(function) => function,
            other => {
                return Err(LangError::runtime(format!(
                    "'{name}' guarda {}, não uma função",
                    other.type_name()
                )));
            }
        };
        if arguments.len() != function.parameters.len() {
            return Err(LangError::runtime(format!(
                "{} esperava {} argumentos, recebeu {}",
                function.name,
                function.parameters.len(),
                arguments.len()
            )));
        }
        let values = arguments
            .iter()
            .map(|argument| self.evaluate(argument))
            .collect::<Result<Vec<_>>>()?;
        self.environment.push_scope();
        let result = (|| {
            for (parameter, value) in function.parameters.iter().zip(values) {
                self.environment.define(parameter.clone(), value)?;
            }
            for statement in &function.body {
                if let Flow::Return(value) = self.execute(statement)? {
                    return Ok(value);
                }
            }
            Ok(Value::Nil)
        })();
        self.environment.pop_scope();
        result
    }
}

impl Default for Interpreter {
    fn default() -> Self {
        Self::new()
    }
}

fn numbers(left: Value, right: Value, operation: impl Fn(f64, f64) -> f64) -> Result<Value> {
    match (left, right) {
        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(operation(a, b))),
        (a, b) => Err(LangError::runtime(format!(
            "operação numérica recebeu {} e {}",
            a.type_name(),
            b.type_name()
        ))),
    }
}

fn compare(left: Value, right: Value, operation: impl Fn(f64, f64) -> bool) -> Result<Value> {
    match (left, right) {
        (Value::Number(a), Value::Number(b)) => Ok(Value::Bool(operation(a, b))),
        (a, b) => Err(LangError::runtime(format!(
            "comparação numérica recebeu {} e {}",
            a.type_name(),
            b.type_name()
        ))),
    }
}

fn type_error(message: &str, value: &Value) -> LangError {
    LangError::runtime(format!("{message}; recebeu {}", value.type_name()))
}
11
src/errors.rs

Separa erros por etapa e localização

Ver código completo de src/errors.rs
use std::fmt;

pub type Result<T> = std::result::Result<T, LangError>;

#[derive(Debug, Clone, PartialEq)]
pub struct LangError {
    pub stage: &'static str,
    pub line: usize,
    pub column: usize,
    pub message: String,
}

impl LangError {
    pub fn new(
        stage: &'static str,
        line: usize,
        column: usize,
        message: impl Into<String>,
    ) -> Self {
        Self { stage, line, column, message: message.into() }
    }

    pub fn runtime(message: impl Into<String>) -> Self {
        Self::new("runtime", 0, 0, message)
    }
}

impl fmt::Display for LangError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.line == 0 {
            write!(formatter, "erro de {}: {}", self.stage, self.message)
        } else {
            write!(
                formatter,
                "erro de {} na linha {}, coluna {}: {}",
                self.stage, self.line, self.column, self.message
            )
        }
    }
}

impl std::error::Error for LangError {}
12
src/main.rs

Lê um arquivo .pulso e mostra sua saída

Ver código completo de src/main.rs
use pulso::run;
use std::{env, fs, process};

fn main() {
    if let Err(error) = execute() {
        eprintln!("{error}");
        process::exit(1);
    }
}

fn execute() -> Result<(), Box<dyn std::error::Error>> {
    let path = env::args().nth(1).ok_or("uso: pulso <arquivo.pulso>")?;
    let source = fs::read_to_string(path)?;
    for line in run(&source)? {
        println!("{line}");
    }
    Ok(())
}
13
examples/boas-vindas.pulso

Exercita toda a linguagem em um programa

Ver código completo de examples/boas-vindas.pulso
funcao dobro(numero) {
    retorne numero * 2;
}

var contador = 1;

enquanto (contador <= 4) {
    var resultado = dobro(contador);

    se (resultado == 6) {
        mostre("chegamos ao seis");
    } senao {
        mostre(resultado);
    }

    contador = contador + 1;
}
14
tests/language.rs

Valida tokens, recursos, escopos e erros

Ver código completo de tests/language.rs
use pulso::{lexer::Lexer, run, token::TokenKind};

#[test]
fn lexer_recognizes_words_numbers_and_operators() {
    let tokens = Lexer::new("var total = 10 + 2;").scan().unwrap();
    assert!(matches!(&tokens[0].kind, TokenKind::Var));
    assert!(matches!(&tokens[1].kind, TokenKind::Identifier(name) if name == "total"));
    assert!(matches!(&tokens[3].kind, TokenKind::Number(10.0)));
    assert!(matches!(&tokens[4].kind, TokenKind::Plus));
}

#[test]
fn variables_conditions_loops_and_functions_work_together() {
    let source = r#"
        funcao dobro(numero) {
            retorne numero * 2;
        }

        var contador = 1;
        enquanto (contador <= 3) {
            se (contador == 2) {
                mostre("meio");
            } senao {
                mostre(dobro(contador));
            }
            contador = contador + 1;
        }
    "#;
    assert_eq!(run(source).unwrap(), vec!["2", "meio", "6"]);
}

#[test]
fn a_block_has_its_own_scope() {
    let source = r#"
        var nome = "fora";
        {
            var nome = "dentro";
            mostre(nome);
        }
        mostre(nome);
    "#;
    assert_eq!(run(source).unwrap(), vec!["dentro", "fora"]);
}

#[test]
fn errors_identify_the_pipeline_stage() {
    let lexical = run("@").unwrap_err();
    assert_eq!(lexical.stage, "léxico");

    let syntax = run("var numero = ;").unwrap_err();
    assert_eq!(syntax.stage, "sintático");

    let runtime = run("mostre(inexistente);").unwrap_err();
    assert_eq!(runtime.stage, "runtime");
}

18 / DO ARQUIVO VAZIO AO PROGRAMA RODANDO

Agora podemos observar cada etapa separadamente.

cargo test
cargo run -- examples/boas-vindas.pulso

O programa de exemplo produz:

2
4
chegamos ao seis
8
01remova um ponto e vírgulaobserve o parser localizar o erro
02troque + por *acompanhe a precedência mudar a AST
03crie um escopodeclare o mesmo nome dentro e fora
04chame com poucos argumentosveja a validação de aridade
05misture texto e númeroprovoque um erro de runtime
06crie uma função recursivaobserve cada chamada ganhar um escopo

Ao final, a função run resume tudo sem esconder nada:

pub fn run(source: &str) -> Result<Vec<String>> {
    let tokens = Lexer::new(source).scan()?;
    let program = Parser::new(tokens).parse()?;
    Interpreter::new().run(&program)
}

Três linhas, depois de construirmos todos os significados por trás delas. Essa é a diferença entre código mágico e abstração: a abstração fica curta porque entendemos e separamos cada responsabilidade.

A IDEIA CENTRAL

Uma linguagem não é compreendida pelo computador; ela é implementada por programas que transformam texto até chegar a ações executáveis.

19 / PERGUNTAS FREQUENTES

Dúvidas que deixam de parecer mágicas quando enxergamos o pipeline.

O computador entende diretamente o código-fonte?

Não. O texto precisa ser analisado e transformado. Na Pulso, o lexer cria tokens, o parser organiza uma AST e o interpretador executa essa estrutura.

O que é um token?

Token é uma unidade classificada do código, como número, identificador, operador ou palavra reservada. Ele preserva o significado útil e descarta detalhes como espaços.

Lexer e parser são a mesma coisa?

Não. O lexer reconhece as peças do texto; o parser verifica como essas peças podem ser combinadas e constrói a estrutura do programa.

O que é uma AST?

AST é uma árvore que representa a estrutura do código. Ela registra relações como soma, condição, chamada de função e bloco sem carregar pontuação desnecessária.

Por que a AST é uma árvore?

Porque uma expressão pode conter expressões menores. Em 2 + 3 * 4, a soma possui como filho o número 2 e como outro filho a multiplicação de 3 por 4.

O que é runtime?

Runtime é o conjunto de regras e estruturas necessárias enquanto o programa executa: valores, variáveis, escopos, chamadas, retornos e erros de execução.

O que é escopo?

Escopo é a região em que um nome existe. Um bloco ou função pode criar uma variável local sem substituir permanentemente outra variável de mesmo nome que esteja fora dele.

Interpretador e compilador usam lexer e parser?

Normalmente sim. Ambos podem compartilhar tokens, parser e AST. Depois da AST, o interpretador executa; o compilador produz outra representação, como bytecode ou código de máquina.

Qual é a diferença entre erro léxico, sintático e semântico?

O erro léxico nasce de um caractere inválido; o sintático nasce de uma combinação que não segue a gramática; o semântico ou de runtime ocorre quando a estrutura é válida, mas a operação não faz sentido.

A linguagem Pulso é funcional de verdade?

Sim. O projeto executa arquivos com números, textos, booleanos, variáveis, operadores, if, while, funções, parâmetros, return e escopos.

O que este artigo fez você pensar?

Dúvidas, experiências e contrapontos ajudam a próxima pessoa a enxergar o assunto por outro ângulo.

Todos passam por moderação. Ao enviar, você concorda com a política de privacidade.

Receba os próximos artigos.

Uma mensagem quando uma nova investigação estiver pronta. Só isso.