1 Módulo 01: Projeto do Professor — O Esqueleto do Compilador e a Escolha da Linguagem
Este é o projeto de referência resolvido pelo professor: as mesmas atividades que cada grupo vai executar neste módulo, feitas por inteiro, com as decisões justificadas uma a uma. Sirva-se dele como modelo do que o seu próprio projeto deve parecer — não como algo a copiar. O domínio da sua linguagem é outro, e as decisões que você tomar sobre ele serão suas.
1.1 Visão Geral do Módulo 01
O módulo 1 pede quatro coisas de cada grupo: escolher e justificar o domínio da linguagem que vai construir, escrever exemplos de como os programas dessa linguagem devem parecer, configurar o ambiente de trabalho sob o modo estrito de compilação, e criar o repositório com o esqueleto do projeto. Nenhuma dessas tarefas parece técnica, e todas condicionam o semestre inteiro.
Vamos resolvê-las aqui na ordem em que elas realmente acontecem. Primeiro decidimos o que construir, porque tudo depende disso. Depois escrevemos os programas de exemplo, que são o teste mais rápido de que a ideia se sustenta. Só então configuramos o ambiente e escrevemos o primeiro código — que, neste módulo, não compila linguagem nenhuma: é a infraestrutura sobre a qual as fases serão montadas a partir do módulo 3.
Além das quatro tarefas, o módulo tem dois tópicos teóricos que admitem código e que resolvemos com exemplos de referência próprios: a distinção entre compilar e interpretar, e a decomposição do compilador em fases. Os demais tópicos — a hierarquia de gramáticas e as distinções entre compilador, interpretador e montador no plano conceitual — são panorâmicos e não têm implementação neste ponto; volto a eles no fim, explicando por que não forcei código onde ele não caberia.
1.2 Tarefa 1: Escolher e justificar o domínio da linguagem
A atividade — escolher um domínio, demonstrar que ele atende ao contrato de capacidades e defender a escolha.
A decisão precisa satisfazer seis exigências simultâneas: símbolos léxicos de mais de uma categoria com pelo menos uma descrita por padrões, uma construção aninhada de profundidade arbitrária, nomes declarados em um ponto e usados em outro, mais de um tipo de valor com pelo menos uma operação restrita a um deles, efeito observável na execução, e tamanho pequeno.
Minha escolha é uma linguagem para reconhecimento de padrões em texto, que chamo de Peneira. O usuário declara padrões e escreve regras que reagem ao que for encontrado num fluxo de entrada — um mini-grep com condições, se você quiser uma imagem rápida.
Escolhi esse domínio por uma razão que vale explicar, porque é o tipo de raciocínio que espero ver na defesa de cada grupo. Nesta disciplina existe um risco específico: os autômatos podem virar um detalhe escondido dentro do analisador léxico, e o compilador levar o crédito inteiro. Num domínio de reconhecimento de padrões isso não acontece, porque o produto da compilação é ele mesmo um motor de autômatos. A teoria aparece duas vezes no mesmo artefato — uma para reconhecer os símbolos da própria Peneira, outra para reconhecer os padrões que o usuário declarou — e o mesmo módulo de construção de autômatos serve às duas. Essa reutilização é a espinha do projeto e o argumento que fecha a disciplina.
Confrontando com o contrato: as categorias léxicas serão nomes, números, cadeias de texto, literais de padrão e sinais de pontuação, e o literal de padrão é descrito por padrões, o que satisfaz a primeira exigência. O aninhamento vem das expressões de condição, em que uma condição contém outra sem limite de profundidade. Os nomes declarados e usados são os padrões: declarados com pattern, usados em on. Os tipos são número e texto, e a extração de valor numérico só se aplica a um deles. O efeito observável é a emissão de resultados. E o tamanho é pequeno: a especificação inteira cabe em duas páginas.
Onde é fácil errar aqui. O erro mais comum não é escolher um domínio ruim, é escolher um domínio bom e grande demais. Toda ideia interessante quer crescer: assim que decidi a Peneira, a tentação foi acrescentar funções definidas pelo usuário, variáveis mutáveis e laços. Cada um desses acréscimos parece pequeno e custa semanas na fase semântica. Cortei todos. A pergunta de controle que uso é direta: consigo escrever a gramática inteira em uma página? Se não, o escopo está grande.
1.3 Tarefa 2: Escrever os programas de exemplo
A atividade — produzir dois ou três exemplos da aparência pretendida dos programas, antes de qualquer formalização.
Esta é a tarefa que os grupos mais subestimam, e é a mais barata em relação ao que revela. Escrever o programa antes da gramática é prática corrente em projeto de linguagens, e o motivo é que a sintaxe que parecia boa na cabeça costuma parecer ruim no papel.
Este é o programa que quero conseguir escrever na Peneira:
Repare no que esse exemplo já decide, mesmo sem gramática nenhuma escrita. Decide que padrões são declarados antes de usados, e portanto que haverá uma tabela de símbolos com uma ordem de declaração a verificar. Decide que a condição é opcional, com a palavra where, o que significa uma produção com parte opcional na gramática. Decide que a extração do valor numérico é explícita, com value, em vez de conversão automática — o que me poupa de inventar regras de coerção de tipo na análise semântica. E decide que a ação é uma emissão com rótulo, o que dá o efeito observável exigido pelo contrato.
Nenhuma dessas decisões foi tomada por análise formal. Todas saíram de escrever o programa e olhar para ele. É por isso que a tarefa vem antes da gramática, e não depois.
Onde é fácil errar aqui. Escrever um exemplo que você gostaria de ler, e não um que consiga compilar. Sintaxe elegante costuma esconder ambiguidade. Quando escrevi a primeira versão, a condição não tinha a palavra where — era só on numero(n) value(n) > 100. Parecia mais limpo e é ambíguo: sem uma marca separando a ligação da condição, o analisador não sabe onde uma termina e a outra começa sem olhar arbitrariamente para a frente. A palavra where custa cinco caracteres ao usuário e me poupa um problema real no módulo 10.
1.4 Tarefa 3: Configurar o ambiente sob o modo estrito
A atividade — deixar o ambiente compilando e executando sob a configuração estrita exigida pela disciplina.
O projeto é construído em C++ com o compilador da Microsoft, a partir do VS Code, e usa CMake para o build. A escolha do CMake tem uma razão prática: as flags estritas ficam declaradas num único lugar, e qualquer verificação automática que eu queira acrescentar depois invoca o mesmo build que eu uso no dia a dia, sem duplicar configuração.
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(peneira LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Os fontes crescem por módulo: cada módulo acrescenta seus arquivos aqui e
# preserva os anteriores. Nada é removido ao longo do semestre.
add_executable(peneira
01_main.cpp
01_source.cpp
01_diagnostico.cpp
01_traducao.cpp
02_cadeia.cpp
02_linguagem.cpp
02_regex.cpp
02_lexico.cpp
02_demos.cpp
03_afd.cpp
03_reconhecedores.cpp
03_demos.cpp
04_afn.cpp
04_notacao.cpp
04_thompson.cpp
04_demos.cpp
05_determinizacao.cpp
05_minimizacao.cpp
05_dot.cpp
05_demos.cpp
06_fechamento.cpp
06_bombeamento.cpp
06_demos.cpp
07_token.cpp
07_lexer.cpp
07_demos.cpp
08_gramatica.cpp
08_derivacao.cpp
08_gramaticas.cpp
08_demos.cpp
10_transformacao.cpp
10_conjuntos.cpp
10_ast.cpp
10_parser.cpp
10_demos.cpp
12_tipos.cpp
12_simbolos.cpp
12_atributos.cpp
12_sema.cpp
12_demos.cpp
13_ri.cpp
13_objeto.cpp
13_ambiente.cpp
13_demos.cpp
14_codegen.cpp
14_vm.cpp
14_demos.cpp
15_blocos.cpp
15_otimizacao.cpp
15_validacao.cpp
15_demos.cpp
)
# Modo estrito exigido pela disciplina. /WX é o que transforma a regra de
# tipagem forte em verificação real: sem ele, os avisos viram texto que
# ninguém lê.
if(MSVC)
target_compile_options(peneira PRIVATE /W4 /WX /permissive- /utf-8)
else()
target_compile_options(peneira PRIVATE -Wall -Wextra -Werror -pedantic)
endif()Vale explicar cada flag, porque elas não são decoração. A que fixa o padrão da linguagem evita que o compilador escolha um padrão antigo por conta própria, o que faria o mesmo código se comportar de forma diferente em máquinas diferentes. A que eleva o nível de avisos liga as verificações que apanham conversão silenciosa e variável não usada. A que desliga as extensões não-padrão da Microsoft impede que eu escreva, sem perceber, código que só compila neste compilador.
E há a que transforma todo aviso em erro. Essa é a única que realmente importa, e vou defendê-la porque é a que mais incomoda no começo. Sem ela, as outras três produzem texto que ninguém lê. Em um projeto que cresce por quinze módulos, o aviso ignorado no módulo 3 é o defeito que aparece no módulo 12, quando o código que o causou já saiu da sua memória. O custo de mantê-la é alto na primeira semana e cai rápido; o custo de não mantê-la só aparece quando é caro pagar.
Também deixei o bloco alternativo para compiladores que não sejam o da Microsoft. Não porque a disciplina os use, mas porque essa é a diferença entre um projeto que só funciona na minha máquina e um que qualquer pessoa consegue construir.
Onde é fácil errar aqui. Deixar o modo estrito para depois, “quando o código estiver pronto”. Não funciona: ligar as flags sobre uma base já escrita produz dezenas de erros de uma vez, e a reação natural é desligá-las de novo. Ligue no primeiro arquivo, quando o custo de conformar-se é uma linha.
Como verificar. O build precisa terminar sem nenhuma linha de aviso. Não “com poucos avisos” — com nenhum. Se a compilação imprimiu algo além dos nomes dos arquivos, há trabalho a fazer.
1.5 Tarefa 4: O esqueleto do projeto
A atividade — criar o repositório e o primeiro código, sobre o qual todas as fases serão montadas.
Aqui vem a decisão de projeto mais consequente do módulo, e ela é invisível para quem olha rápido: o que escrever primeiro, se nenhuma fase do compilador existe ainda?
A resposta que escolhi é escrever a infraestrutura que todas as fases vão usar, e que é sempre a mesma em qualquer compilador: saber onde as coisas estão no arquivo de entrada, e saber reclamar de forma útil quando algo dá errado. Poderia ter começado pelo analisador léxico e enfiado o tratamento de posição dentro dele, como muitos projetos fazem. Não fiz porque o tratamento de posição é usado por todas as fases, e infraestrutura compartilhada enfiada dentro de uma fase específica é dívida garantida.
1.5.1 O arquivo-fonte e as posições
01_source.h
#ifndef PENEIRA_01_SOURCE_H
#define PENEIRA_01_SOURCE_H
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
namespace peneira {
// Posição de um caractere no arquivo-fonte. O deslocamento é o índice bruto
// dentro do texto; linha e coluna são contadas a partir de 1, como espera
// qualquer pessoa lendo uma mensagem de erro.
struct Position {
std::size_t offset;
std::size_t line;
std::size_t column;
};
// Guarda o texto de um programa Peneira e o índice de início de cada linha.
// O índice é construído uma única vez, no carregamento: sem ele, converter um
// deslocamento em linha e coluna exigiria varrer o texto do começo a cada erro
// reportado.
class SourceFile {
public:
static SourceFile fromText(std::string name, std::string text);
static std::optional<SourceFile> loadFromDisk(const std::string& path,
std::string& error);
const std::string& name() const noexcept;
const std::string& text() const noexcept;
std::size_t length() const noexcept;
std::size_t lineCount() const noexcept;
// Converte deslocamento em linha e coluna por busca binária no índice.
Position positionAt(std::size_t offset) const;
// Texto de uma linha (contada a partir de 1), sem o terminador.
// Usado para exibir a linha ofensora abaixo da mensagem de erro.
std::string lineText(std::size_t line) const;
private:
SourceFile(std::string name, std::string text);
void indexLines();
std::string name_;
std::string text_;
std::vector<std::size_t> lineStarts_;
};
} // namespace peneira
#endif // PENEIRA_01_SOURCE_HA classe carrega o texto inteiro na memória e indexa, no carregamento, o deslocamento de início de cada linha. Vale explicar por que o índice existe. Toda mensagem de erro precisa dizer linha e coluna, mas as fases trabalham com deslocamentos brutos — o analisador léxico sabe que o problema está no caractere de número 847, não na linha 23. Converter uma coisa na outra sem índice exige varrer o texto do começo a cada erro. Com o índice, é uma busca binária.
01_source.cpp
#include "01_source.h"
#include <algorithm>
#include <fstream>
#include <iterator>
#include <sstream>
#include <utility>
namespace peneira {
SourceFile::SourceFile(std::string name, std::string text)
: name_(std::move(name)), text_(std::move(text)), lineStarts_() {
indexLines();
}
SourceFile SourceFile::fromText(std::string name, std::string text) {
return SourceFile(std::move(name), std::move(text));
}
std::optional<SourceFile> SourceFile::loadFromDisk(const std::string& path,
std::string& error) {
// Abrimos em modo binário de propósito: em modo texto o Windows converte
// CRLF em LF durante a leitura, e os deslocamentos deixariam de bater com
// os bytes do arquivo. Quem trata o CR é o indexador de linhas.
std::ifstream input(path, std::ios::binary);
if (!input) {
error = "não foi possível abrir o arquivo";
return std::nullopt;
}
std::ostringstream buffer;
buffer << input.rdbuf();
if (input.bad()) {
error = "falha de leitura do arquivo";
return std::nullopt;
}
error.clear();
return SourceFile(path, buffer.str());
}
void SourceFile::indexLines() {
lineStarts_.clear();
lineStarts_.push_back(0);
for (std::size_t i = 0; i < text_.size(); ++i) {
if (text_[i] == '\n') {
lineStarts_.push_back(i + 1);
}
}
}
const std::string& SourceFile::name() const noexcept { return name_; }
const std::string& SourceFile::text() const noexcept { return text_; }
std::size_t SourceFile::length() const noexcept { return text_.size(); }
std::size_t SourceFile::lineCount() const noexcept { return lineStarts_.size(); }
Position SourceFile::positionAt(std::size_t offset) const {
const std::size_t clamped = std::min(offset, text_.size());
// upper_bound devolve o primeiro início de linha estritamente maior que o
// deslocamento; a linha que contém o deslocamento é a anterior. Como
// lineStarts_[0] é sempre 0 e clamped nunca é negativo, o iterador nunca
// é begin(), e o decremento abaixo é seguro.
const auto it = std::upper_bound(lineStarts_.begin(), lineStarts_.end(), clamped);
const auto distance = std::distance(lineStarts_.begin(), it);
const std::size_t index = static_cast<std::size_t>(distance) - 1;
return Position{clamped, index + 1, clamped - lineStarts_[index] + 1};
}
std::string SourceFile::lineText(std::size_t line) const {
if (line == 0 || line > lineStarts_.size()) {
return std::string();
}
const std::size_t start = lineStarts_[line - 1];
std::size_t end = (line < lineStarts_.size()) ? lineStarts_[line] : text_.size();
// Recua sobre o terminador de linha para que a linha exibida no
// diagnóstico não arraste CR nem LF.
while (end > start && (text_[end - 1] == '\n' || text_[end - 1] == '\r')) {
--end;
}
return text_.substr(start, end - start);
}
} // namespace peneiraTrês detalhes da implementação merecem comentário, porque são armadilhas reais.
O arquivo é aberto em modo binário de propósito. Em modo texto, o Windows converte a sequência de retorno de carro e nova linha em apenas nova linha durante a leitura, e os deslocamentos deixam de corresponder aos bytes do arquivo. Quem for depurar comparando com um editor hexadecimal vai encontrar diferença. Quem trata o retorno de carro é o extrator de linha, na hora de exibir.
A conversão de deslocamento em posição usa uma busca pelo primeiro início de linha estritamente maior que o deslocamento, e recua um. O recuo é seguro porque o índice sempre começa com zero e o deslocamento nunca é negativo, então o iterador nunca aponta para o começo do vetor — anotei isso em comentário porque é exatamente o tipo de raciocínio que se perde e vira suspeita de defeito seis módulos depois.
E o resultado da subtração de iteradores é convertido explicitamente para o tipo sem sinal. Sem a conversão explícita, o compilador em modo estrito recusa a compilação. Esse é o tipo de atrito que o modo estrito impõe, e é também o tipo de conversão que, deixada implícita, produz defeito silencioso quando o valor é grande.
1.5.2 Os diagnósticos
01_diagnostico.h
#ifndef PENEIRA_01_DIAGNOSTICO_H
#define PENEIRA_01_DIAGNOSTICO_H
#include <cstddef>
#include <cstdint>
#include <iosfwd>
#include <string>
#include <vector>
#include "01_source.h"
namespace peneira {
enum class Severity : std::uint8_t { Error, Warning, Note };
// Um diagnóstico é sempre ancorado numa posição do fonte. Diagnóstico sem
// posição é diagnóstico inútil: o usuário sabe que algo deu errado e não sabe
// onde.
struct Diagnostic {
Severity severity;
Position position;
std::string message;
};
// Coleta os diagnósticos de todas as fases em vez de abortar no primeiro.
// A partir do analisador léxico, cada fase reporta o que encontrar e segue;
// quem decide parar é o programa principal, olhando hasErrors().
class DiagnosticBag {
public:
void report(Severity severity, Position position, std::string message);
void error(Position position, std::string message);
void warning(Position position, std::string message);
bool hasErrors() const noexcept;
std::size_t errorCount() const noexcept;
std::size_t size() const noexcept;
const std::vector<Diagnostic>& all() const noexcept;
// Imprime no formato "arquivo:linha:coluna: severidade: mensagem",
// seguido da linha ofensora e de um cursor sob a coluna.
void printAll(const SourceFile& source, std::ostream& out) const;
private:
std::vector<Diagnostic> items_;
std::size_t errorCount_ = 0;
};
const char* severityLabel(Severity severity) noexcept;
} // namespace peneira
#endif // PENEIRA_01_DIAGNOSTICO_HA decisão de projeto embutida aqui é que os diagnósticos são coletados, não lançados. Nenhuma fase aborta no primeiro problema; cada uma reporta o que encontrar e segue adiante, e quem decide parar é o programa principal. Isso parece exagero num módulo em que não há fase nenhuma, e é o que torna possível, no módulo 10, um analisador sintático que reporta três erros de uma vez em vez de obrigar o usuário a corrigir e recompilar três vezes.
01_diagnostico.cpp
#include "01_diagnostico.h"
#include <ostream>
#include <utility>
namespace peneira {
const char* severityLabel(Severity severity) noexcept {
switch (severity) {
case Severity::Error:
return "erro";
case Severity::Warning:
return "aviso";
case Severity::Note:
return "nota";
}
// Inalcançável para um valor válido do enum, mas o compilador em modo
// estrito exige um retorno em todos os caminhos.
return "desconhecido";
}
void DiagnosticBag::report(Severity severity, Position position, std::string message) {
if (severity == Severity::Error) {
++errorCount_;
}
items_.push_back(Diagnostic{severity, position, std::move(message)});
}
void DiagnosticBag::error(Position position, std::string message) {
report(Severity::Error, position, std::move(message));
}
void DiagnosticBag::warning(Position position, std::string message) {
report(Severity::Warning, position, std::move(message));
}
bool DiagnosticBag::hasErrors() const noexcept { return errorCount_ > 0; }
std::size_t DiagnosticBag::errorCount() const noexcept { return errorCount_; }
std::size_t DiagnosticBag::size() const noexcept { return items_.size(); }
const std::vector<Diagnostic>& DiagnosticBag::all() const noexcept { return items_; }
void DiagnosticBag::printAll(const SourceFile& source, std::ostream& out) const {
for (const Diagnostic& item : items_) {
out << source.name() << ':' << item.position.line << ':' << item.position.column
<< ": " << severityLabel(item.severity) << ": " << item.message << '\n';
const std::string line = source.lineText(item.position.line);
if (line.empty()) {
continue;
}
out << " " << line << '\n' << " ";
// O cursor precisa alinhar com a coluna do erro. Copiamos o caractere
// original quando ele é tabulação, para que o alinhamento sobreviva a
// fontes e larguras de tabulação diferentes.
for (std::size_t i = 0; i + 1 < item.position.column && i < line.size(); ++i) {
out << (line[i] == '\t' ? '\t' : ' ');
}
out << "^\n";
}
}
} // namespace peneiraA impressão segue o formato consagrado — arquivo, linha, coluna, severidade, mensagem — porque editores sabem interpretá-lo e transformar a saída em navegação clicável. Abaixo da mensagem vem a linha ofensora e um cursor apontando a coluna. Repare que o cursor copia a tabulação do original em vez de substituí-la por espaço: sem isso, o alinhamento quebra em qualquer arquivo indentado com tabulação, e é um defeito clássico de mensagens de erro.
Repare também no retorno final da função que converte severidade em texto, depois do switch que já cobre todos os casos do enumerado. Ele é inalcançável para qualquer valor válido, e o modo estrito exige que exista de qualquer forma. Deixei um comentário dizendo isso, para que ninguém o remova achando que é descuido.
1.5.3 O programa principal
01_main.cpp
#include <exception>
#include <iostream>
#include <string>
#include <vector>
#include "01_diagnostico.h"
#include "01_source.h"
#include "01_traducao.h"
#include "02_demos.h"
#include "03_demos.h"
#include "04_demos.h"
#include "05_demos.h"
#include "06_demos.h"
#include "07_demos.h"
#include "08_demos.h"
#include "10_ast.h"
#include "10_demos.h"
#include "10_parser.h"
#include "12_demos.h"
#include "12_sema.h"
#include "13_demos.h"
#include "13_objeto.h"
#include "14_codegen.h"
#include "14_demos.h"
#include "14_vm.h"
#include "15_demos.h"
#include "15_otimizacao.h"
#include "15_validacao.h"
namespace {
void mostrarUso(const std::string& programa) {
std::cout << "uso: " << programa << " <arquivo.pen>\n"
<< " " << programa << " --demo (modulo 1: compilar x interpretar)\n"
<< " " << programa << " --cadeias (modulo 2: operacoes sobre cadeias)\n"
<< " " << programa << " --linguagens (modulo 2: operacoes sobre linguagens)\n"
<< " " << programa << " --regex (modulo 2: expressoes regulares)\n"
<< " " << programa << " --lexico (modulo 2: especificacao lexica)\n"
<< " " << programa << " --afd (modulo 3: tabelas de transicao)\n"
<< " " << programa << " --tracar (modulo 3: configuracoes instantaneas)\n"
<< " " << programa << " --completude (modulo 3: estado de erro explicito)\n"
<< " " << programa << " --verificar (modulo 3: corpora do modulo 2)\n"
<< " " << programa << " --notacao (modulo 4: analisador da notacao)\n"
<< " " << programa << " --fecho (modulo 4: fecho vazio)\n"
<< " " << programa << " --thompson (modulo 4: construcao de Thompson)\n"
<< " " << programa << " --confronto (modulo 4: gerado x manual)\n"
<< " " << programa << " --pipeline (modulo 5: as tres etapas)\n"
<< " " << programa << " --refinamento (modulo 5: particoes de Moore)\n"
<< " " << programa << " --unicidade (modulo 5: automato minimo e unico)\n"
<< " " << programa << " --piorcaso (modulo 5: explosao exponencial)\n"
<< " " << programa << " --dot (modulo 5: exporta diagramas)\n"
<< " " << programa << " --fechamento (modulo 6: uniao, intersecao, complemento)\n"
<< " " << programa << " --bombeamento (modulo 6: lema do bombeamento)\n"
<< " " << programa << " --aninhamento (modulo 6: o limite do modelo regular)\n"
<< " " << programa << " --classes (modulo 6: classes de equivalencia)\n"
<< " " << programa << " --corpora (modulo 7: as seis categorias verificadas)\n"
<< " " << programa << " --lexer (modulo 7: analise lexica completa)\n"
<< " " << programa << " --desempates (modulo 7: mais longo e prioridade)\n"
<< " " << programa << " --erros (modulo 7: erro lexico e recuperacao)\n"
<< " " << programa << " --retrocesso (modulo 7: custo da releitura)\n"
<< " " << programa << " --gramatica (modulo 8: a gramatica da Peneira)\n"
<< " " << programa << " --ambiguidade (modulo 8: contando arvores)\n"
<< " " << programa << " --condicional (modulo 8: o senao pendente)\n"
<< " " << programa << " --simplificar (modulo 8: simbolos inuteis)\n"
<< " " << programa << " --arvores (modulo 8: derivacoes da Peneira)\n"
<< " " << programa << " --preparar (modulo 10: transformacoes da gramatica)\n"
<< " " << programa << " --ll1 (modulo 10: primeiros, seguidores e tabela)\n"
<< " " << programa << " --parser (modulo 10: analise sintatica)\n"
<< " " << programa << " --assoc (modulo 10: associatividade reconstruida)\n"
<< " " << programa << " --recuperar (modulo 10: erro sintatico)\n"
<< " " << programa << " --frontend (modulo 10: texto ate arvore)\n"
<< " " << programa << " --semantica (modulo 12: sintatico x semantico)\n"
<< " " << programa << " --simbolos (modulo 12: tabela de simbolos)\n"
<< " " << programa << " --escopos (modulo 12: duas estrategias de escopo)\n"
<< " " << programa << " --tipos (modulo 12: inferencia por automato)\n"
<< " " << programa << " --atributos (modulo 12: sintetizados e herdados)\n"
<< " " << programa << " --semerros (modulo 12: erros semanticos)\n"
<< " " << programa << " --porqueri (modulo 13: por que ha camada intermediaria)\n"
<< " " << programa << " --formasri (modulo 13: arvore, pos-fixada, tres enderecos)\n"
<< " " << programa << " --curtocircuito (modulo 13: desvios e preenchimento retroativo)\n"
<< " " << programa << " --objeto (modulo 13: formato e modelo de execucao)\n"
<< " " << programa << " --ambiente (modulo 13: mapa de memoria e ativacao)\n"
<< " " << programa << " --ativacoes (modulo 13: cadeias de controle e de acesso)\n"
<< " " << programa << " --selecao (modulo 14: os tres subproblemas)\n"
<< " " << programa << " --referencias (modulo 14: resolucao de referencias)\n"
<< " " << programa << " --registradores (modulo 14: interferencia e coloracao)\n"
<< " " << programa << " --gerar (modulo 14: o objeto gerado)\n"
<< " " << programa << " --executar (modulo 14: executor minimo)\n"
<< " " << programa << " --pontaaponta (modulo 14: o compilador inteiro)\n"
<< " " << programa << " --blocos (modulo 15: blocos basicos e fluxo)\n"
<< " " << programa << " --vivacidade (modulo 15: fluxo de dados por ponto fixo)\n"
<< " " << programa << " --otimizar (modulo 15: otimizacoes locais)\n"
<< " " << programa << " --insegura (modulo 15: transformacoes que quebram)\n"
<< " " << programa << " --integracao (modulo 15: validacao e execucao)\n"
<< " " << programa << " --retrospectiva (modulo 15: o caminho inteiro)\n";
}
// Relatório das fases do compilador. No Módulo 1 nenhuma delas existe ainda;
// o esqueleto declara a decomposição que o semestre inteiro vai preencher,
// e cada módulo troca uma linha "pendente" por trabalho de verdade.
void relatarFases(const peneira::SourceFile& fonte) {
std::cout << "arquivo: " << fonte.name() << '\n'
<< " " << fonte.length() << " caracteres, " << fonte.lineCount()
<< " linha(s)\n\n"
<< "fases do compilador:\n"
<< " analise lexica pronta (modulo 7)\n"
<< " analise sintatica pronta (modulo 10)\n"
<< " analise semantica pronta (modulo 12)\n"
<< " geracao de codigo pronta (modulo 14)\n"
<< " otimizacao pronta (modulo 15)\n"
<< " execucao pronta (modulo 15)\n";
}
int compilarArquivo(const std::string& caminho) {
std::string erro;
const std::optional<peneira::SourceFile> fonte =
peneira::SourceFile::loadFromDisk(caminho, erro);
if (!fonte.has_value()) {
std::cerr << caminho << ": erro: " << erro << '\n';
return 1;
}
peneira::DiagnosticBag diagnosticos;
if (fonte->length() == 0) {
diagnosticos.error(fonte->positionAt(0), "arquivo de entrada vazio");
diagnosticos.printAll(*fonte, std::cerr);
return 1;
}
// A partir do módulo 12 o comando principal roda o compilador de verdade,
// e não mais o relatório de fases pendentes. A ordem é a do pipeline:
// léxico e sintático (módulos 7 e 10) produzem a árvore, e a semântica
// (este módulo) a verifica.
std::size_t errosSintaticos = 0;
const peneira::AstPtr raiz =
peneira::analisarArquivo(*fonte, diagnosticos, errosSintaticos);
// A semântica só roda se houver árvore. Rodá-la sobre uma árvore
// gravemente incompleta produziria erros semânticos que são consequência
// do erro sintático, e não defeitos do programa — ruído que esconde a
// causa real.
if (raiz && errosSintaticos == 0) {
const peneira::ResultadoSemantico semantico =
peneira::verificarArquivo(*fonte, *raiz, diagnosticos);
if (diagnosticos.size() > 0) {
diagnosticos.printAll(*fonte, std::cerr);
}
if (semantico.errosSemanticos > 0) {
std::cerr << semantico.errosSemanticos << " erro(s) semantico(s)\n";
return 1;
}
// A partir do módulo 14 o compilador produz saída de verdade, e do 15
// ela passa pelo otimizador antes de ser emitida.
peneira::RelatorioDeGeracao relatorio;
peneira::EstatisticasDeOtimizacao estatisticas;
const peneira::ProgramaObjeto objeto =
peneira::gerarObjetoOtimizado(semantico, *raiz, relatorio,
estatisticas);
for (const std::string& v : relatorio.violacoesDeUsoUnico) {
std::cerr << "erro interno de geracao: " << v << '\n';
}
if (!relatorio.violacoesDeUsoUnico.empty()) {
return 2;
}
// Validação estática antes de executar: as condições de erro previstas
// na especificação são verificadas em todos os caminhos, e não só nos
// que a entrada percorrer. Um objeto que não passa aqui não deveria
// sequer ser gravado.
const std::vector<peneira::ProblemaNoObjeto> problemas =
peneira::validarObjeto(objeto);
for (const peneira::ProblemaNoObjeto& p : problemas) {
std::cerr << "objeto malformado em " << p.onde << ": " << p.mensagem
<< '\n';
}
if (!problemas.empty()) {
return 2;
}
// O objeto é gravado ao lado do fonte, para inspeção.
const std::string caminhoObjeto = caminho + ".obj.txt";
std::string erroGravacao;
if (!peneira::gravarObjeto(objeto, caminhoObjeto, erroGravacao)) {
std::cerr << caminhoObjeto << ": erro: " << erroGravacao << '\n';
return 1;
}
relatarFases(*fonte);
std::cout << " padroes compilados: " << semantico.padroes.size()
<< ", regras: " << relatorio.regrasGeradas
<< ", instrucoes: " << relatorio.instrucoesEmitidas << '\n'
<< " objeto gravado em: " << caminhoObjeto << '\n';
// Entrada pela entrada padrão, conforme a interface prevista no design:
// `peneira programa.pen < entrada.txt`. Sem redirecionamento, não há o
// que executar, e o comando se comporta como compilador puro.
std::string entrada;
std::string linha;
while (std::getline(std::cin, linha)) {
entrada += linha;
entrada.push_back('\n');
}
if (entrada.empty()) {
return 0;
}
const peneira::ResultadoExecucao execucao =
peneira::executar(objeto, entrada);
std::cout << "\nexecucao sobre " << execucao.bytesLidos
<< " bytes de entrada:\n";
for (const peneira::Emissao& e : execucao.emissoes) {
std::cout << " " << e.rotulo << "\t" << e.valor << '\n';
}
std::cout << " " << execucao.emissoes.size() << " emissao(oes) em "
<< execucao.casamentos << " casamento(s)\n";
for (const std::string& erroExec : execucao.erros) {
std::cerr << "erro de execucao: " << erroExec << '\n';
}
return execucao.erros.empty() ? 0 : 1;
}
diagnosticos.printAll(*fonte, std::cerr);
std::cerr << errosSintaticos << " erro(s) sintatico(s)\n";
return 1;
}
int rodarDemonstracao() {
const peneira::demo::NoPtr expressao = peneira::demo::expressaoDeExemplo();
std::cout << "expressao: (2 + 3) * 4 - 5\n\n";
std::cout << "caminho do interpretador\n"
<< " percorre a arvore e produz o resultado agora\n"
<< " resultado: " << peneira::demo::avaliar(*expressao) << "\n\n";
const std::vector<peneira::demo::Instrucao> programa =
peneira::demo::compilar(*expressao);
std::cout << "caminho do compilador\n"
<< " percorre a arvore e produz instrucoes para depois\n"
<< peneira::demo::desmontar(programa)
<< " resultado da execucao: " << peneira::demo::executar(programa) << '\n';
return 0;
}
} // namespace
int main(int argc, char** argv) {
const std::vector<std::string> args(argv, argv + static_cast<std::size_t>(argc));
const std::string programa = args.empty() ? std::string("peneira") : args[0];
if (args.size() != 2) {
mostrarUso(programa);
return 2;
}
try {
if (args[1] == "--demo") {
return rodarDemonstracao();
}
if (args[1] == "--cadeias") {
peneira::demo::mostrarOperacoesDeCadeia(std::cout);
return 0;
}
if (args[1] == "--linguagens") {
peneira::demo::mostrarOperacoesDeLinguagem(std::cout);
return 0;
}
if (args[1] == "--regex") {
peneira::demo::mostrarExpressoesRegulares(std::cout);
return 0;
}
if (args[1] == "--lexico") {
peneira::demo::mostrarEspecificacaoLexica(std::cout);
return 0;
}
if (args[1] == "--afd") {
peneira::demo::mostrarAutomatos(std::cout);
return 0;
}
if (args[1] == "--tracar") {
peneira::demo::mostrarConfiguracoes(std::cout);
return 0;
}
if (args[1] == "--completude") {
peneira::demo::mostrarCompletude(std::cout);
return 0;
}
if (args[1] == "--verificar") {
peneira::demo::mostrarVerificacao(std::cout);
return 0;
}
if (args[1] == "--notacao") {
peneira::demo::mostrarAnaliseDaNotacao(std::cout);
return 0;
}
if (args[1] == "--fecho") {
peneira::demo::mostrarFechoVazio(std::cout);
return 0;
}
if (args[1] == "--thompson") {
peneira::demo::mostrarThompson(std::cout);
return 0;
}
if (args[1] == "--confronto") {
peneira::demo::mostrarConfronto(std::cout);
return 0;
}
if (args[1] == "--pipeline") {
peneira::demo::mostrarPipeline(std::cout);
return 0;
}
if (args[1] == "--refinamento") {
peneira::demo::mostrarRefinamento(std::cout);
return 0;
}
if (args[1] == "--unicidade") {
peneira::demo::mostrarUnicidade(std::cout);
return 0;
}
if (args[1] == "--piorcaso") {
peneira::demo::mostrarPiorCaso(std::cout);
return 0;
}
if (args[1] == "--dot") {
peneira::demo::exportarDiagramas(std::cout);
return 0;
}
if (args[1] == "--fechamento") {
peneira::demo::mostrarFechamento(std::cout);
return 0;
}
if (args[1] == "--bombeamento") {
peneira::demo::mostrarBombeamento(std::cout);
return 0;
}
if (args[1] == "--aninhamento") {
peneira::demo::mostrarAninhamento(std::cout);
return 0;
}
if (args[1] == "--classes") {
peneira::demo::mostrarClassesDeEquivalencia(std::cout);
return 0;
}
if (args[1] == "--corpora") {
peneira::demo::mostrarCorporaCompletos(std::cout);
return 0;
}
if (args[1] == "--lexer") {
peneira::demo::mostrarAnaliseLexica(std::cout);
return 0;
}
if (args[1] == "--desempates") {
peneira::demo::mostrarDesempates(std::cout);
return 0;
}
if (args[1] == "--erros") {
peneira::demo::mostrarErrosLexicos(std::cout);
return 0;
}
if (args[1] == "--retrocesso") {
peneira::demo::mostrarRetrocesso(std::cout);
return 0;
}
if (args[1] == "--gramatica") {
peneira::demo::mostrarGramaticaPeneira(std::cout);
return 0;
}
if (args[1] == "--ambiguidade") {
peneira::demo::mostrarAmbiguidade(std::cout);
return 0;
}
if (args[1] == "--condicional") {
peneira::demo::mostrarCondicional(std::cout);
return 0;
}
if (args[1] == "--simplificar") {
peneira::demo::mostrarSimplificacao(std::cout);
return 0;
}
if (args[1] == "--arvores") {
peneira::demo::mostrarArvoresDaPeneira(std::cout);
return 0;
}
if (args[1] == "--preparar") {
peneira::demo::mostrarTransformacoes(std::cout);
return 0;
}
if (args[1] == "--ll1") {
peneira::demo::mostrarConjuntosELL1(std::cout);
return 0;
}
if (args[1] == "--parser") {
peneira::demo::mostrarAnaliseSintatica(std::cout);
return 0;
}
if (args[1] == "--assoc") {
peneira::demo::mostrarAssociatividade(std::cout);
return 0;
}
if (args[1] == "--recuperar") {
peneira::demo::mostrarRecuperacaoSintatica(std::cout);
return 0;
}
if (args[1] == "--frontend") {
peneira::demo::mostrarFrontEndCompleto(std::cout);
return 0;
}
if (args[1] == "--semantica") {
peneira::demo::mostrarSintaticoVersusSemantico(std::cout);
return 0;
}
if (args[1] == "--simbolos") {
peneira::demo::mostrarTabelaDeSimbolos(std::cout);
return 0;
}
if (args[1] == "--escopos") {
peneira::demo::mostrarEstrategiasDeEscopo(std::cout);
return 0;
}
if (args[1] == "--tipos") {
peneira::demo::mostrarInferenciaDeTipos(std::cout);
return 0;
}
if (args[1] == "--atributos") {
peneira::demo::mostrarAtributos(std::cout);
return 0;
}
if (args[1] == "--semerros") {
peneira::demo::mostrarErrosSemanticos(std::cout);
return 0;
}
if (args[1] == "--porqueri") {
peneira::demo::mostrarPorQueIntermediaria(std::cout);
return 0;
}
if (args[1] == "--formasri") {
peneira::demo::mostrarFormasIntermediarias(std::cout);
return 0;
}
if (args[1] == "--curtocircuito") {
peneira::demo::mostrarCurtoCircuito(std::cout);
return 0;
}
if (args[1] == "--objeto") {
peneira::demo::mostrarFormatoObjeto(std::cout);
return 0;
}
if (args[1] == "--ambiente") {
peneira::demo::mostrarAmbienteDeExecucao(std::cout);
return 0;
}
if (args[1] == "--ativacoes") {
peneira::demo::mostrarRegistrosDeAtivacao(std::cout);
return 0;
}
if (args[1] == "--selecao") {
peneira::demo::mostrarSelecaoDeInstrucoes(std::cout);
return 0;
}
if (args[1] == "--referencias") {
peneira::demo::mostrarResolucaoDeReferencias(std::cout);
return 0;
}
if (args[1] == "--registradores") {
peneira::demo::mostrarAlocacaoDeRegistradores(std::cout);
return 0;
}
if (args[1] == "--gerar") {
peneira::demo::mostrarObjetoGerado(std::cout);
return 0;
}
if (args[1] == "--executar") {
peneira::demo::mostrarExecucao(std::cout);
return 0;
}
if (args[1] == "--pontaaponta") {
peneira::demo::mostrarPontaAPonta(std::cout);
return 0;
}
if (args[1] == "--blocos") {
peneira::demo::mostrarBlocosBasicos(std::cout);
return 0;
}
if (args[1] == "--vivacidade") {
peneira::demo::mostrarVivacidade(std::cout);
return 0;
}
if (args[1] == "--otimizar") {
peneira::demo::mostrarOtimizacoes(std::cout);
return 0;
}
if (args[1] == "--insegura") {
peneira::demo::mostrarTransformacaoInsegura(std::cout);
return 0;
}
if (args[1] == "--integracao") {
peneira::demo::mostrarIntegracaoFinal(std::cout);
return 0;
}
if (args[1] == "--retrospectiva") {
peneira::demo::mostrarRetrospectiva(std::cout);
return 0;
}
return compilarArquivo(args[1]);
} catch (const std::exception& e) {
std::cerr << "erro interno: " << e.what() << '\n';
return 3;
}
}O programa aceita um arquivo e imprime o relatório de fases, todas pendentes, com o módulo em que cada uma vai nascer. Isso é deliberado e serve a dois propósitos. Torna a decomposição em fases visível e concreta desde o primeiro dia, em vez de ser um diagrama abstrato na aula teórica. E dá um marcador de progresso: a cada módulo, uma linha de “pendente” vira trabalho de verdade.
A única verificação que este módulo já consegue fazer é reclamar de um arquivo vazio. Ela existe menos pela utilidade e mais como prova de que a infraestrutura de diagnóstico funciona ponta a ponta antes de haver qualquer fase que a use. Rodando sobre um arquivo vazio, a saída é a mensagem posicionada em linha 1, coluna 1, com o código de retorno indicando falha.
Onde é fácil errar aqui. Escrever o esqueleto grande demais. A tentação é criar todas as classes que o compilador vai ter — o analisador léxico vazio, o analisador sintático vazio, uma hierarquia de nós de árvore que ainda não se sabe qual será. Todo esse código é apagado depois, porque o formato real só se descobre quando a fase é implementada. Escrevi só o que já tem uso concreto hoje.
Como verificar. O executável precisa compilar sem aviso, aceitar um arquivo de exemplo e imprimir o relatório, e reclamar de forma posicionada sobre um arquivo vazio. Nesta versão de referência, o programa de exemplo tem duzentos e vinte caracteres em oito linhas, e é isso que o relatório informa.
1.6 Referência teórica: compilar e interpretar
O módulo apresenta a distinção entre compiladores, interpretadores, montadores e tradutores. É um tópico que costuma ficar em definições decoradas, e resolvi torná-lo concreto com um exemplo mínimo — código de referência que não pertence ao compilador da Peneira e vive claramente separado dele.
A ideia é pegar a mesma árvore de expressão e submetê-la aos dois tratamentos. O interpretador percorre a árvore e produz o resultado agora. O compilador percorre a mesma árvore e produz instruções para depois. Os dois chegam ao mesmo número, por caminhos que não se parecem.
01_traducao.h
#ifndef PENEIRA_01_TRADUCAO_H
#define PENEIRA_01_TRADUCAO_H
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
// Demonstração isolada do Módulo 1: a diferença entre interpretar e compilar,
// sobre a mesma árvore de expressão. Não faz parte do compilador da Peneira —
// é o exemplo mínimo que torna concreta a distinção apresentada na teoria.
// A partir do Módulo 13 o mesmo par de ideias reaparece, aí sim dentro do
// sistema principal.
namespace peneira::demo {
enum class Operador : std::uint8_t { Somar, Subtrair, Multiplicar };
enum class TipoNo : std::uint8_t { Literal, Operacao };
struct No;
using NoPtr = std::unique_ptr<No>;
struct No {
TipoNo tipo;
double valor; // significativo quando tipo == TipoNo::Literal
Operador operador; // significativo quando tipo == TipoNo::Operacao
NoPtr esquerda;
NoPtr direita;
};
NoPtr literal(double valor);
NoPtr operacao(Operador operador, NoPtr esquerda, NoPtr direita);
// Caminho do interpretador: percorre a árvore e produz o resultado agora.
double avaliar(const No& no);
enum class OpCode : std::uint8_t { PushConst, Add, Sub, Mul };
struct Instrucao {
OpCode opcode;
double operando; // significativo apenas para PushConst
};
// Caminho do compilador: percorre a árvore e produz instruções para depois.
std::vector<Instrucao> compilar(const No& no);
// Máquina de pilha que executa o que o compilador emitiu.
double executar(const std::vector<Instrucao>& programa);
std::string desmontar(const std::vector<Instrucao>& programa);
// Monta a expressão usada na demonstração: (2 + 3) * 4 - 5.
NoPtr expressaoDeExemplo();
} // namespace peneira::demo
#endif // PENEIRA_01_TRADUCAO_H01_traducao.cpp
#include "01_traducao.h"
#include <sstream>
#include <stdexcept>
#include <utility>
namespace peneira::demo {
namespace {
// Emite em pós-ordem: primeiro os dois operandos, depois a operação. É a ordem
// natural para uma máquina de pilha, porque quando a instrução de operação
// executa os dois valores já estão empilhados.
void emitir(const No& no, std::vector<Instrucao>& saida) {
if (no.tipo == TipoNo::Literal) {
saida.push_back(Instrucao{OpCode::PushConst, no.valor});
return;
}
emitir(*no.esquerda, saida);
emitir(*no.direita, saida);
switch (no.operador) {
case Operador::Somar:
saida.push_back(Instrucao{OpCode::Add, 0.0});
break;
case Operador::Subtrair:
saida.push_back(Instrucao{OpCode::Sub, 0.0});
break;
case Operador::Multiplicar:
saida.push_back(Instrucao{OpCode::Mul, 0.0});
break;
}
}
const char* nomeOpCode(OpCode opcode) noexcept {
switch (opcode) {
case OpCode::PushConst:
return "PUSH_CONST";
case OpCode::Add:
return "ADD";
case OpCode::Sub:
return "SUB";
case OpCode::Mul:
return "MUL";
}
return "???";
}
} // namespace
NoPtr literal(double valor) {
NoPtr no = std::make_unique<No>();
no->tipo = TipoNo::Literal;
no->valor = valor;
no->operador = Operador::Somar;
return no;
}
NoPtr operacao(Operador operador, NoPtr esquerda, NoPtr direita) {
NoPtr no = std::make_unique<No>();
no->tipo = TipoNo::Operacao;
no->valor = 0.0;
no->operador = operador;
no->esquerda = std::move(esquerda);
no->direita = std::move(direita);
return no;
}
double avaliar(const No& no) {
if (no.tipo == TipoNo::Literal) {
return no.valor;
}
const double esquerda = avaliar(*no.esquerda);
const double direita = avaliar(*no.direita);
switch (no.operador) {
case Operador::Somar:
return esquerda + direita;
case Operador::Subtrair:
return esquerda - direita;
case Operador::Multiplicar:
return esquerda * direita;
}
throw std::logic_error("operador desconhecido na avaliacao");
}
std::vector<Instrucao> compilar(const No& no) {
std::vector<Instrucao> programa;
emitir(no, programa);
return programa;
}
double executar(const std::vector<Instrucao>& programa) {
std::vector<double> pilha;
pilha.reserve(programa.size());
for (const Instrucao& instrucao : programa) {
if (instrucao.opcode == OpCode::PushConst) {
pilha.push_back(instrucao.operando);
continue;
}
if (pilha.size() < 2) {
throw std::logic_error("pilha insuficiente para a operacao");
}
const double direita = pilha.back();
pilha.pop_back();
const double esquerda = pilha.back();
pilha.pop_back();
switch (instrucao.opcode) {
case OpCode::Add:
pilha.push_back(esquerda + direita);
break;
case OpCode::Sub:
pilha.push_back(esquerda - direita);
break;
case OpCode::Mul:
pilha.push_back(esquerda * direita);
break;
case OpCode::PushConst:
break; // já tratado acima
}
}
if (pilha.size() != 1) {
throw std::logic_error("programa nao deixou exatamente um resultado na pilha");
}
return pilha.back();
}
std::string desmontar(const std::vector<Instrucao>& programa) {
std::ostringstream saida;
for (std::size_t i = 0; i < programa.size(); ++i) {
saida << " " << i << ": " << nomeOpCode(programa[i].opcode);
if (programa[i].opcode == OpCode::PushConst) {
saida << ' ' << programa[i].operando;
}
saida << '\n';
}
return saida.str();
}
NoPtr expressaoDeExemplo() {
// (2 + 3) * 4 - 5
NoPtr soma = operacao(Operador::Somar, literal(2.0), literal(3.0));
NoPtr produto = operacao(Operador::Multiplicar, std::move(soma), literal(4.0));
return operacao(Operador::Subtrair, std::move(produto), literal(5.0));
}
} // namespace peneira::demoRodando sobre a expressão (2 + 3) \times 4 - 5, o interpretador devolve 15 diretamente. O compilador produz sete instruções — empilha 2, empilha 3, soma, empilha 4, multiplica, empilha 5, subtrai — e a máquina de pilha, executando essas sete instruções, também devolve 15.
O que quero que fique visível é que a emissão é em pós-ordem: primeiro os dois operandos, depois a operação. Essa ordem não é arbitrária, é a única que funciona numa máquina de pilha, porque quando a instrução de operação executa os dois valores já precisam estar empilhados. É a mesma ordem que vamos usar no módulo 14, aí sim para valer, gerando o código objeto da Peneira. Quem entender o percurso aqui, com sete instruções, entende lá com setenta.
A distinção conceitual que o exemplo materializa é a de quando o trabalho acontece. O interpretador funde análise e execução num passo só; o compilador as separa, paga o custo da tradução uma vez e permite que a execução aconteça muitas vezes depois, possivelmente em outra máquina. Montadores e tradutores entre linguagens de alto nível são pontos dessa mesma escala, e sistemas reais misturam as abordagens com frequência — uma máquina virtual que compila trechos quentes durante a execução é as duas coisas ao mesmo tempo.
Onde é fácil errar aqui. Ao implementar a máquina de pilha, a ordem de desempilhar. O segundo operando sai primeiro, porque foi o último a entrar. Trocar a ordem não muda nada na soma e na multiplicação, e produz resultado errado na subtração — defeito que passa em metade dos testes e é achado tarde. Verifiquei justamente com uma subtração no exemplo, e não por acaso.
1.7 Referência teórica: as fases e a separação entre análise e síntese
O outro tópico com componente implementável é a decomposição em fases. Ele já está materializado no relatório do programa principal, e vale explicitar o argumento que a estrutura carrega.
As fases se dividem em duas metades. A análise vai do texto até uma representação verificada do programa — analisador léxico, sintático e semântico. A síntese vai dessa representação até o código executável — geração de código e execução. A separação existe por um argumento de engenharia com ganho combinatório: com uma representação intermediária no meio, suportar cinco linguagens de origem e cinco máquinas de destino custa dez peças, e não vinte e cinco.
Não escrevi código para essa divisão neste módulo, e a razão é honesta: qualquer estrutura que eu criasse agora para representá-la seria descartada quando as fases reais nascessem. O relatório de fases pendentes cumpre o papel didático sem produzir código que vai ser apagado.
1.8 Tópicos deste módulo sem código de referência
Dois tópicos do módulo 1 não têm implementação, e registro isso explicitamente para que não pareça omissão.
A hierarquia de gramáticas e linguagens é apresentada em nível panorâmico: quatro tipos de gramáticas, quatro classes de linguagens, quatro modelos de máquina. Não há o que implementar sobre uma classificação, e forçar código aqui produziria um exemplo artificial. Os dois níveis que a disciplina percorre ganham implementação nos módulos 3 a 5 e 9 a 10, quando os modelos correspondentes forem construídos.
A distinção conceitual entre compilador, interpretador, montador e tradutor está parcialmente coberta pelo exemplo de tradução acima, que materializa a diferença entre os dois primeiros. Montadores e tradutores entre linguagens de alto nível ficam no plano conceitual — implementar um montador exigiria um conjunto de instruções concreto, que só definimos no módulo 14.
1.9 Verificação da entrega
| Item | Como conferir | Estado nesta referência |
|---|---|---|
| Domínio escolhido e justificado | O texto demonstra as seis capacidades do contrato, uma a uma | Atende |
| Programas de exemplo | Existe pelo menos um programa completo da linguagem pretendida | Atende |
| Ambiente sob modo estrito | O build termina sem nenhuma linha de aviso | Atende, verificado por compilação |
| Executável funcionando | Aceita arquivo, imprime relatório, reclama de arquivo vazio com posição | Atende, verificado por execução |
| Repositório estruturado | Instruções gerais na raiz, documentos e código separados | Atende |
| Tipagem forte e explícita | Nenhum tipo apagado, nenhum descarte de verificação | Atende |
O ponto que quero deixar registrado sobre esta entrega é que ela não compila linguagem nenhuma, e isso está certo. O módulo 1 entrega decisão de projeto e infraestrutura, não funcionalidade. Grupos que tentam antecipar o analisador léxico agora costumam chegar ao módulo 7 com um analisador escrito sobre uma teoria que ainda não conheciam, e tendo de refazê-lo.