LINGUAGENS · INTERPRETADORES · RUST
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.
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.
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.
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.
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.
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.
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.
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.
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 original | Classe do token | Informação guardada |
|---|---|---|
var | palavra reservada | início de declaração |
idade | identificador | o nome “idade” |
= | operador | atribuir/inicializar |
20 | número | o valor 20 |
+ | operador | soma |
1 | número | o valor 1 |
; | delimitador | fim 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)
Semicolon07 / 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.
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;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>,
},
}Nó é 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.
| Camada | Operadores | Chama a camada mais forte |
|---|---|---|
| ou | ou | e |
| e | e | igualdade |
| 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.
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.
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.
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)
}
}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.
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.
| Etapa | Exemplo | O que sabemos |
|---|---|---|
| léxica | var preço = @; | o caractere não pertence ao vocabulário |
| sintática | var = 10; | os tokens não seguem a gramática |
| runtime/semântica | 10 + "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 textoGuardar 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 é 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.
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.rsBaixar o projeto completo da Pulso 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.
Cargo.tomlDeclara 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]
src/lib.rsConecta 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)
}
src/token.rsDefine 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 }
}
}
src/lexer.rsTransforma 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()
}
src/ast.rsModela 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,
}
src/parser.rsAplica 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) }
}
src/value.rsRepresenta 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"),
}
}
}
src/environment.rsGuarda 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()
}
}
src/runtime.rsTransporta 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),
}
src/interpreter.rsPercorre 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()))
}
src/errors.rsSepara 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 {}
src/main.rsLê 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(())
}
examples/boas-vindas.pulsoExercita 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;
}
tests/language.rsValida 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.pulsoO programa de exemplo produz:
2
4
chegamos ao seis
8Ao 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.
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.
A CONVERSA CONTINUA
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.