Moacyr Francischetti Corrêa

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:

contatos.pen
pattern email  = /[a-z0-9._]+@[a-z]+\.[a-z]+/;
pattern numero = /-?[0-9]+(\.[0-9]+)?/;

rule {
    on email(e)                        => emit("contato", e);
    on numero(n) where value(n) > 100  => emit("grande", n);
}

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_H

A 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 peneira

Trê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_H

A 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 peneira

A 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_H
01_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::demo

Rodando 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.