1 Análise semântica — Projeto do Professor

Este é o projeto de referência do professor — as tarefas do Projeto Integrador deste módulo resolvidas do começo ao fim, com as decisões justificadas uma a uma. É o modelo do que cada grupo deve produzir no próprio projeto, e existe para ser estudado, não copiado: os tipos são os da sua linguagem, e as condições de invalidez são as que ela prevê. O que se copia daqui é o método — escrever a regra antes do verificador, tratar a mensagem como parte do produto, e exigir da bateria de testes as duas metades, a que recusa o inválido e a que deixa passar o válido.

1.1 Visão Geral

Há uma frase que resume este módulo melhor do que qualquer descrição de suas partes: on emial(e) => emit("contato", e); está sintaticamente perfeita. O analisador do capítulo anterior a aceita sem uma queixa, e faz bem, porque a gramática fala de forma e isto é uma questão de contexto — emial nunca foi declarado, e descobrir isso exige lembrar do que foi lido vinte linhas antes. Toda esta fase existe nesse vão entre a forma e o significado.

O que muda de natureza aqui é a estrutura de dados, e a mudança custa mais do que o volume de código sugere. As fases anteriores produziam objetos que correspondiam a trechos do texto: o reconhecedor de símbolos devolvia o que estava escrito, a árvore devolvia como o que estava escrito se organizava, e as duas podiam ser conferidas olhando um trecho por vez. A tabela de símbolos não corresponde a trecho algum. Ela existe entre os trechos, e responde perguntas que nenhum deles responde isoladamente: este nome já apareceu? onde? o que ele era quando apareceu? ainda vale aqui? A consequência prática é a que mais surpreende quem implementa pela primeira vez — um defeito na tabela não aparece no nó em que está, aparece no nó em que alguém consulta, e os dois podem estar longe um do outro.

A solução se organiza em torno das três tarefas, e cada uma resolve uma pergunta distinta. A primeira constrói o conhecimento acumulado, com escopo aninhado, porque a Peneira tem dois níveis e não um. A segunda escreve o sistema de tipos, e o escreve na especificação antes de escrevê-lo no código — a ordem é a decisão, não a formalidade. A terceira trata a mensagem de recusa como parte do produto, com uma explicação própria para cada uma das catorze condições que a linguagem prevê.

Uma observação sobre a aridade, que a teoria do módulo lista ao lado da declaração e dos tipos e que aqui não rende verificação. Nesta linguagem, emit tem exatamente dois argumentos e value exatamente um, e quem impõe isso é a gramática: uma chamada com o número errado de argumentos não chega à análise semântica, porque o analisador sintático a recusou antes. Isso não torna a aridade irrelevante — torna-a um exemplo do critério que decide onde cada verificação mora. Uma propriedade que a forma captura deve ser verificada pela forma, e duplicá-la aqui produziria duas recusas para o mesmo defeito, em fases diferentes, com mensagens que envelhecem em ritmos distintos. Em uma linguagem com funções declaradas pelo usuário, a aridade migraria para cá pelo mesmo critério: deixaria de ser propriedade da forma no instante em que passasse a depender de uma declaração anterior.

Vale registrar de saída os números que este módulo persegue, porque eles reaparecem em cada seção adiante. Sobre a descrição defeituosa de referência, o verificador produz catorze diagnósticos numa única execução — dez erros e quatro avisos —, cobrindo treze espécies distintas; sobre a descrição correta, produz zero. Os dois resultados são condição de saída da bateria de testes, e o segundo é o mais difícil dos dois.

1.2 Tarefa 1: Registrar e consultar os nomes declarados

O que a tarefa pede

Fazer o sistema manter conhecimento acumulado sobre a descrição inteira, e não apenas sobre a posição corrente da leitura: os nomes declarados passam a ser registrados quando aparecem e consultados quando são usados, e uma referência a nome inexistente é recusada. É a primeira estrutura do percurso que não corresponde a nenhum trecho específico do texto de entrada.

12_symtab.h
// 12_symtab.h — O conhecimento acumulado sobre a descrição inteira.
//
// ESTA É A PRIMEIRA ESTRUTURA DO PERCURSO QUE NÃO CORRESPONDE A NENHUM TRECHO DO
// TEXTO DE ENTRADA. O reconhecedor de símbolos devolve o que está escrito; a
// árvore devolve como o que está escrito se organiza; esta tabela existe ENTRE os
// trechos, e responde perguntas que nenhum trecho isolado responde — este nome já
// apareceu antes? onde? o que ele era quando apareceu? ainda vale aqui?
//
// A CONSEQUÊNCIA PRÁTICA da mudança de natureza é a que mais custa a quem
// implementa pela primeira vez: as fases anteriores podiam ser verificadas
// olhando um trecho por vez, e esta não pode. Um defeito aqui não aparece no nó
// em que está — aparece no nó em que alguém consulta, que pode estar a vinte
// linhas de distância.
//
// ESCOPO, E POR QUE ELE NÃO É DECORAÇÃO NESTA LINGUAGEM. A Peneira tem dois
// níveis, e não um: os `pattern` são globais, e a ligação que cada `on` cria vive
// só dentro da própria ação. Dois níveis bastam para que a tabela precise ser uma
// PILHA de escopos e não um mapa — o mesmo nome pode ser ligação numa ação e não
// existir na seguinte, e uma tabela plana só saberia dizer "existe em algum
// lugar", que é precisamente a resposta errada.
//
// PONTEIROS E VALIDADE. `declarar` pode realocar o escopo corrente, e por isso
// nenhum ponteiro devolvido por `consultar` sobrevive a uma declaração posterior.
// A ordem de uso do verificador respeita isso: declara-se primeiro, consulta-se
// depois. O comentário está aqui porque a regra é invisível na assinatura.

#ifndef PENEIRA_12_SYMTAB_H
#define PENEIRA_12_SYMTAB_H

#include <cstddef>
#include <string>
#include <vector>

#include "07_lexer.h"
#include "12_tipos.h"

namespace peneira {

enum class EspecieDeSimbolo {
    Padrao,
    Ligacao,
};

std::string nomeDaEspecie(EspecieDeSimbolo especie);

struct Simbolo {
    std::string nome;
    EspecieDeSimbolo especie = EspecieDeSimbolo::Padrao;
    Tipo tipo = Tipo::Indefinido;
    Posicao declaracao;
    std::size_t nivel = 0;
    std::size_t usos = 0;

    // Preenchidos quando a espécie é `Padrao`; a ligação criada por um `on` os
    // HERDA do padrão que a produziu, no instante da declaração. Herdar em vez de
    // consultar depois não é economia: dentro da ação, uma ligação homônima ao
    // padrão o oculta, e a consulta tardia encontraria a si mesma.
    std::string expressao;   // o corpo da expressão regular, sem as barras
    bool compilou = false;   // a expressão é de fato uma expressão regular
    bool numerico = false;   // L(padrao) esta contida no formato numerico
    bool decidido = false;   // a contenção pôde ser decidida
    std::string testemunha;  // a cadeia que prova que o padrão não é numérico

    // Preenchido apenas quando a espécie é `Ligacao`: de qual padrão ela vem.
    // É o campo que sustenta a verificação de `value(x)` — sem ele, saber-se-ia
    // que `x` é um casamento e não de quê, que é saber quase nada.
    std::string padraoDeOrigem;
};

class TabelaDeNomes {
public:
    TabelaDeNomes();

    void abrirEscopo();
    void fecharEscopo();
    std::size_t nivelAtual() const;

    // Devolve `nullptr` quando a declaração foi aceita; devolve o símbolo já
    // existente NO MESMO escopo quando é redeclaração. Assim quem chama tem, de
    // graça, a posição da declaração anterior para pôr na mensagem.
    const Simbolo* declarar(const Simbolo& simbolo);

    Simbolo* consultar(const std::string& nome);
    const Simbolo* consultar(const std::string& nome) const;
    Simbolo* consultarNoEscopoAtual(const std::string& nome);

    // O nome declarado mais próximo do que se procurou, quando a distância é
    // pequena o bastante para que a sugestão ajude em vez de confundir.
    std::string nomeMaisParecido(const std::string& procurado, EspecieDeSimbolo especie) const;

    // Todos os símbolos que já existiram, incluindo os de escopos encerrados. É o
    // que permite relatar o que foi declarado e nunca usado depois que a
    // travessia terminou.
    std::vector<Simbolo> historico() const;

    std::string formatar() const;

private:
    std::vector<std::vector<Simbolo>> escopos_;
    std::vector<Simbolo> encerrados_;
};

std::size_t distanciaDeEdicao(const std::string& primeira, const std::string& segunda);

}  // namespace peneira

#endif  // PENEIRA_12_SYMTAB_H
12_symtab.cpp
#include "12_symtab.h"

#include <algorithm>
#include <sstream>

namespace peneira {
namespace {

// recorte:inicio sugestao-so-quando-perto
// Distância mínima de edição, com duas linhas em vez da matriz inteira. Ela
// existe por um motivo estreito: transformar "nome nao declarado" em "nome nao
// declarado; talvez `email`". A sugestão só é oferecida quando a distância é
// pequena — sugerir qualquer coisa é pior do que não sugerir, porque manda quem
// lê investigar uma pista falsa.
std::size_t menorDosTres(const std::size_t a, const std::size_t b, const std::size_t c) {
// recorte:fim sugestao-so-quando-perto
    return std::min(a, std::min(b, c));
}

}  // namespace

std::string nomeDaEspecie(const EspecieDeSimbolo especie) {
    return especie == EspecieDeSimbolo::Padrao ? "padrao" : "ligacao";
}

std::size_t distanciaDeEdicao(const std::string& primeira, const std::string& segunda) {
    const std::size_t linhas = primeira.size();
    const std::size_t colunas = segunda.size();

    std::vector<std::size_t> anterior(colunas + 1, 0);
    std::vector<std::size_t> atual(colunas + 1, 0);
    for (std::size_t coluna = 0; coluna <= colunas; ++coluna) {
        anterior[coluna] = coluna;
    }

    for (std::size_t linha = 1; linha <= linhas; ++linha) {
        atual[0] = linha;
        for (std::size_t coluna = 1; coluna <= colunas; ++coluna) {
            const std::size_t custo = primeira[linha - 1] == segunda[coluna - 1] ? 0u : 1u;
            atual[coluna] = menorDosTres(anterior[coluna] + 1, atual[coluna - 1] + 1,
                                         anterior[coluna - 1] + custo);
        }
        anterior = atual;
    }
    return anterior[colunas];
}

TabelaDeNomes::TabelaDeNomes() { escopos_.emplace_back(); }

void TabelaDeNomes::abrirEscopo() { escopos_.emplace_back(); }

// recorte:inicio escopo-global-nao-se-fecha
void TabelaDeNomes::fecharEscopo() {
    if (escopos_.size() <= 1) {
        // O escopo global não se fecha. Deixar que se fechasse tornaria possível
        // um desequilíbrio entre aberturas e fechamentos passar despercebido até
        // a primeira consulta devolver "não existe" para um padrão declarado.
        return;
    }
    for (Simbolo& simbolo : escopos_.back()) {
        encerrados_.push_back(simbolo);
    }
    escopos_.pop_back();
}
// recorte:fim escopo-global-nao-se-fecha

std::size_t TabelaDeNomes::nivelAtual() const { return escopos_.size() - 1; }

// recorte:inicio declarar-recusa-o-repetido
const Simbolo* TabelaDeNomes::declarar(const Simbolo& simbolo) {
    if (const Simbolo* anterior = consultarNoEscopoAtual(simbolo.nome)) {
        return anterior;
    }
    Simbolo copia = simbolo;
    copia.nivel = nivelAtual();
    escopos_.back().push_back(std::move(copia));
    return nullptr;
}
// recorte:fim declarar-recusa-o-repetido

// recorte:inicio consulta-de-dentro-para-fora
Simbolo* TabelaDeNomes::consultar(const std::string& nome) {
    // Do escopo mais interno para fora — e é esta ordem, e só ela, que faz uma
    // ligação ocultar um padrão homônimo em vez de conviver com ele.
    for (std::size_t indice = escopos_.size(); indice > 0; --indice) {
        for (Simbolo& simbolo : escopos_[indice - 1]) {
            if (simbolo.nome == nome) {
                return &simbolo;
            }
        }
    }
    return nullptr;
}
// recorte:fim consulta-de-dentro-para-fora

const Simbolo* TabelaDeNomes::consultar(const std::string& nome) const {
    for (std::size_t indice = escopos_.size(); indice > 0; --indice) {
        for (const Simbolo& simbolo : escopos_[indice - 1]) {
            if (simbolo.nome == nome) {
                return &simbolo;
            }
        }
    }
    return nullptr;
}

Simbolo* TabelaDeNomes::consultarNoEscopoAtual(const std::string& nome) {
    for (Simbolo& simbolo : escopos_.back()) {
        if (simbolo.nome == nome) {
            return &simbolo;
        }
    }
    return nullptr;
}

std::string TabelaDeNomes::nomeMaisParecido(const std::string& procurado,
                                            const EspecieDeSimbolo especie) const {
    std::string melhor;
    std::size_t menorDistancia = static_cast<std::size_t>(-1);

    for (const std::vector<Simbolo>& escopo : escopos_) {
        for (const Simbolo& simbolo : escopo) {
            if (simbolo.especie != especie) {
                continue;
            }
            const std::size_t distancia = distanciaDeEdicao(procurado, simbolo.nome);
            if (distancia < menorDistancia) {
                menorDistancia = distancia;
                melhor = simbolo.nome;
            }
        }
    }

    // Duas edições sobre um nome curto já é semelhança demais para ser pista. O
    // limite cresce com o tamanho do nome procurado, porque errar duas letras em
    // `identificador` é muito menos suspeito do que errar duas em `on`.
    const std::size_t limite = procurado.size() <= 3 ? 1u : 2u;
    if (menorDistancia > limite) {
        return {};
    }
    return melhor;
}

std::vector<Simbolo> TabelaDeNomes::historico() const {
    std::vector<Simbolo> todos = encerrados_;
    for (const std::vector<Simbolo>& escopo : escopos_) {
        for (const Simbolo& simbolo : escopo) {
            todos.push_back(simbolo);
        }
    }
    return todos;
}

std::string TabelaDeNomes::formatar() const {
    std::ostringstream saida;
    saida << "  nivel  especie  nome        tipo        usos  observacao\n";
    saida << "  -----  -------  ----------  ----------  ----  ----------\n";

    const std::vector<Simbolo> todos = historico();
    for (const Simbolo& simbolo : todos) {
        std::string observacao;
        if (simbolo.especie == EspecieDeSimbolo::Padrao) {
            if (!simbolo.compilou) {
                observacao = "expressao invalida";
            } else if (!simbolo.decidido) {
                observacao = "numericidade indecidida";
            } else if (simbolo.numerico) {
                observacao = "numerico";
            } else {
                observacao = "nao numerico (ex.: \"" + simbolo.testemunha + "\")";
            }
        } else {
            observacao = "de " + simbolo.padraoDeOrigem;
        }

        std::string especie = nomeDaEspecie(simbolo.especie);
        especie.resize(std::max<std::size_t>(especie.size(), 7), ' ');
        saida << "  " << simbolo.nivel << "      " << especie << "  ";
        std::string nome = simbolo.nome;
        nome.resize(std::max<std::size_t>(nome.size(), 10), ' ');
        saida << nome << "  ";
        std::string tipo = nomeDoTipoDeValor(simbolo.tipo);
        tipo.resize(std::max<std::size_t>(tipo.size(), 10), ' ');
        saida << tipo << "  " << simbolo.usos << "     " << observacao << '\n';
    }
    return saida.str();
}

}  // namespace peneira

A primeira decisão foi não escrever um mapa. A tentação é grande e o argumento a favor é honesto — um mapa de nome para símbolo resolve inserção e consulta em uma linha cada, e a linguagem tem poucos nomes. O que ele não resolve é a pergunta que a Peneira faz o tempo todo: o nome n existe aqui? Um mapa só sabe responder se ele existe em algum lugar, e essa é precisamente a resposta errada, porque a ligação criada por um on vive só dentro da própria ação. Duas ações podem usar n sem qualquer relação entre elas, e uma delas pode não usar nenhum. Uma tabela plana daria por declarado, na segunda ação, um nome que só a primeira declarou.

Por isso a estrutura é uma pilha de escopos, com dois níveis. O nível global guarda os padrões e vale da declaração em diante; o nível da ação é aberto por cada on e fechado quando a ação termina, e contém exatamente um nome. Consulta-se do mais interno para o mais externo, e é essa ordem — só ela — que faz uma ligação ocultar um padrão homônimo em vez de conviver com ele. Dois níveis parecem poucos para justificar uma pilha, e é justamente por serem poucos que o exemplo é bom: a pilha não está aqui porque a linguagem é complicada, está aqui porque a alternativa responde errado já no segundo nível.

O escopo fechado não é descartado. Ele é movido para um histórico, e o motivo é uma verificação que só faz sentido depois que a travessia terminou: quais nomes foram declarados e nunca consultados. Se a tabela esquecesse o escopo ao fechá-lo, essa pergunta ficaria sem como ser feita, e ela é a que pega o caso mais comum de todos — a ligação criada e não usada, que é uma ação emitindo sempre a mesma coisa independentemente do que casou.

Duas decisões menores merecem registro porque foram tomadas contra o hábito. A primeira é que declarar devolve o símbolo anterior quando encontra redeclaração, em vez de devolver apenas verdadeiro ou falso. Devolver o símbolo dá de graça a posição da declaração original, e é o que permite à mensagem dizer onde ela estava em vez de dizer que ela existe. A segunda é que a ligação herda do padrão, no instante em que é declarada, o que se sabe sobre ele — se compilou, se é numérico, qual cadeia prova que não é. Consultar o padrão depois pareceria mais econômico e estaria errado: dentro da ação, uma ligação homônima ao padrão o oculta, e a consulta tardia encontraria a si mesma.

A tabela também é o lugar de uma cortesia que custa vinte linhas e muda a experiência de quem usa o sistema. Quando um nome não é encontrado, o verificador procura o nome declarado mais parecido, por distância mínima de edição, e o oferece — emial produz “talvez email”. O limite é apertado de propósito e cresce com o tamanho do nome procurado: duas edições sobre um nome de duas letras é semelhança demais para ser pista, e sugerir qualquer coisa é pior do que não sugerir, porque manda quem lê investigar uma direção falsa.

Onde é fácil errar. Guardar ponteiros para símbolos através de uma declaração. Inserir num escopo pode realocá-lo, e o ponteiro obtido antes deixa de valer — sem que nada acuse, porque a memória antiga continua legível por um tempo. A ordem de uso do verificador respeita isso, declarando primeiro e consultando depois, e o cabeçalho registra a regra porque ela é invisível na assinatura. Como verificar que está correta: peça à tabela que se imprima ao final de uma descrição válida e confira duas colunas — o nível de cada nome e a contagem de usos. Se um padrão citado por uma ação aparecer com zero usos, a consulta não está encontrando o que deveria; se uma ligação aparecer no nível zero, o escopo não foi aberto.

1.3 Tarefa 2: Verificar a compatibilidade dos tipos

O que a tarefa pede

Verificar as comparações e operações escritas pelo usuário quanto à compatibilidade do que elas relacionam: o que se pode comparar com o quê, e sobre que combinações cada operação faz sentido. A regra precisa estar escrita na especificação da linguagem antes de estar no código — descobrir a regra enquanto se implementa a verificação produz um sistema cujo comportamento ninguém consegue prever sem ler a implementação.

12_tipos.h
// 12_tipos.h — O sistema de tipos da Peneira, escrito como dado consultável.
//
// A DECISÃO QUE ORGANIZA ESTE ARQUIVO está declarada antes de qualquer tipo: a
// regra de compatibilidade não é uma cadeia de `if` espalhada pelo verificador —
// é uma TABELA, e o verificador a consulta. A diferença não é estética. Uma
// cadeia de `if` responde "isto vale?" e não responde "o que vale?", de modo que
// a única maneira de saber o que a linguagem aceita é ler o verificador inteiro
// e reconstruir a regra de cabeça. Com a tabela, acrescentar um tipo é
// acrescentar linhas, e a especificação da linguagem (`docs/12_semantica.md`) é
// legível ao lado dela sem tradução.
//
// A ORDEM TAMBÉM É DECISÃO: a regra foi escrita na especificação ANTES de virar
// código. Descobrir a regra enquanto se implementa a verificação produz um
// sistema cujo comportamento ninguém consegue prever sem ler a implementação —
// e cuja "especificação" é escrita depois, olhando o que o código já faz, o que
// é o oposto de especificar.
//
// O TIPO `Casamento` é o que distingue esta linguagem de um exercício genérico
// de tipos. Ele não é texto: é o trecho da entrada que um `pattern` reconheceu,
// e o que se sabe sobre ele em tempo de compilação é qual padrão o produziu.
// Toda a verificação de `value(x)` sai daí.

#ifndef PENEIRA_12_TIPOS_H
#define PENEIRA_12_TIPOS_H

#include <string>
#include <vector>

#include "03_afd.h"
#include "07_lexer.h"

namespace peneira {

// Os tipos que a linguagem distingue. `Indefinido` não é um tipo do usuário: é o
// marcador de "aqui já houve uma recusa", e existe para que um defeito produza
// UMA mensagem em vez de uma cascata delas. Sem ele, um nome não ligado dentro de
// uma comparação geraria a recusa do nome e, logo em seguida, a recusa da
// comparação por incompatibilidade — dois relatos do mesmo defeito, e o segundo
// apontando para o lugar errado.
enum class Tipo {
    Indefinido,
    Casamento,
    Numero,
    Texto,
    Logico,
};

std::string nomeDoTipoDeValor(Tipo tipo);

// `Casamento` é utilizável onde se espera texto, e essa é a única promoção da
// linguagem. Ela é declarada aqui, num lugar só, porque promoção implícita
// espalhada é a origem clássica do sistema de tipos que ninguém consegue
// descrever.
Tipo normalizar(Tipo tipo);
bool ehTextual(Tipo tipo);

// Os comparadores se separam em duas classes, e a separação é uma decisão de
// projeto da linguagem: igualdade vale sobre texto, ordem não. Ordenar texto
// exige uma regra de colação — maiúsculas antes ou depois, acentos onde — que a
// Peneira não tem motivo para escolher, e escolher em silêncio produziria um
// resultado que varia com a máquina.
enum class ClasseDeOperador {
    Igualdade,
    Ordem,
    NaoComparador,
};

ClasseDeOperador classeDoOperador(TipoDeToken operador);
std::string textoDoOperador(TipoDeToken operador);

// Uma linha da tabela: sobre que par de tipos, com que classe de operador, a
// comparação faz sentido — e o que ela produz.
struct RegraDeComparacao {
    ClasseDeOperador classe = ClasseDeOperador::Igualdade;
    Tipo esquerda = Tipo::Numero;
    Tipo direita = Tipo::Numero;
    Tipo resultado = Tipo::Logico;
};

const std::vector<RegraDeComparacao>& regrasDeComparacao();

// O veredito carrega o que a mensagem de recusa precisa dizer: o que se esperava
// e o que se encontrou. Devolver só `false` obrigaria quem chama a reconstruir a
// explicação, e é assim que nascem as mensagens genéricas.
struct Veredito {
    bool valida = false;
    Tipo resultado = Tipo::Indefinido;
    std::string esperado;
    std::string encontrado;
};

Veredito verificarComparacao(Tipo esquerda, TipoDeToken operador, Tipo direita);
Veredito verificarConectivo(TipoDeToken conectivo, Tipo esquerda, Tipo direita);

// ---------------------------------------------------------------------------
// A classificação numérica de um `pattern`, decidida por autômato.
//
// `value(x)` só faz sentido quando o padrão que ligou `x` produz trechos que são
// números. A linguagem não tem declaração de tipo de padrão — a gramática está
// travada e acrescentar sintaxe para isso seria mudar a linguagem para caber na
// implementação. Então a resposta se DECIDE, e o instrumento já está construído:
// L(p) ⊆ L(numérico) é decidível, e o produto dos dois autômatos é a decisão.
//
// O que o produto devolve quando a resposta é "não" vale tanto quanto a resposta:
// a cadeia mais curta que o padrão aceita e o formato numérico recusa. Ela vira
// a mensagem — "o padrão casa `a@a.a`, que não é número" —, e é a diferença
// entre acusar e explicar.
//
// LIMITE DECLARADO: o coringa `.` é interpretado sobre o alfabeto que a máquina
// conhece, que é o dos símbolos escritos na própria expressão. Um padrão que use
// coringa pode, por isso, ser classificado como numérico sem o ser sobre um
// alfabeto maior. É restrição da construção do autômato, não desta verificação, e
// está registrada aqui porque é aqui que ela produz consequência visível.
// ---------------------------------------------------------------------------

struct Contencao {
    bool decidida = false;  // falso quando um dos autômatos não pôde ser construído
    bool contida = false;
    std::string testemunha;  // a cadeia mais curta que a primeira aceita e a segunda recusa
};

Contencao linguagemContida(const Afd& contida, const Afd& continente);

// O resultado de compilar a expressão de um `pattern` escrito pelo usuário. Ela
// nunca foi verificada até aqui: para o reconhecedor de símbolos ela é apenas o
// trecho entre barras, e é esta fase que descobre se ela é uma expressão regular.
struct PadraoDoUsuario {
    bool ok = false;
    Afd afd;
    std::string erro;  // posição e motivo, quando a expressão não é válida
};

PadraoDoUsuario compilarExpressaoDePadrao(const std::string& corpo);

// O lexema do padrão chega com as barras delimitadoras; o corpo é o que está
// entre elas.
std::string corpoDaExpressao(const std::string& lexemaEntreBarras);

// O formato numérico da linguagem, compilado uma vez. É literalmente a mesma
// expressão que descreve o literal numérico da Peneira — se um dia ela mudar,
// muda num lugar só.
const Afd& afdDoFormatoNumerico();
const std::string& expressaoDoFormatoNumerico();

}  // namespace peneira

#endif  // PENEIRA_12_TIPOS_H
12_tipos.cpp
#include "12_tipos.h"

#include <cstddef>

#include "02_regex.h"
#include "04_afn.h"
#include "05_determinizacao.h"

namespace peneira {
namespace {

constexpr std::size_t kSemPar = static_cast<std::size_t>(-1);

// A união dos dois alfabetos. Ela é necessária porque a contenção precisa ser
// decidida sobre TODO símbolo que qualquer das duas máquinas reconhece: um
// símbolo que só a primeira conhece é exatamente o que pode levá-la a aceitar
// uma cadeia que a segunda recusa, e ignorá-lo devolveria "contida" para um caso
// que não é.
std::string unirAlfabetos(const std::string& primeiro, const std::string& segundo) {
    std::string uniao = primeiro;
    for (const char simbolo : segundo) {
        if (uniao.find(simbolo) == std::string::npos) {
            uniao += simbolo;
        }
    }
    return uniao;
}

// Refaz o caminho da busca em largura, do par encontrado até o par inicial, e
// devolve a cadeia lida. Como a busca é em largura, esta é a MENOR cadeia que
// distingue as duas linguagens — e cadeia curta é o que faz a mensagem caber na
// cabeça de quem lê.
std::string reconstruirCadeia(std::size_t par, const std::vector<std::size_t>& anterior,
                              const std::vector<char>& simboloDeEntrada) {
    std::string invertida;
    while (anterior[par] != kSemPar) {
        invertida += simboloDeEntrada[par];
        par = anterior[par];
    }
    return std::string(invertida.rbegin(), invertida.rend());
}

}  // namespace

std::string nomeDoTipoDeValor(const Tipo tipo) {
    switch (tipo) {
        case Tipo::Indefinido:
            return "indefinido";
        case Tipo::Casamento:
            return "casamento";
        case Tipo::Numero:
            return "numero";
        case Tipo::Texto:
            return "texto";
        case Tipo::Logico:
            return "logico";
    }
    return "indefinido";
}

Tipo normalizar(const Tipo tipo) { return tipo == Tipo::Casamento ? Tipo::Texto : tipo; }

bool ehTextual(const Tipo tipo) { return tipo == Tipo::Texto || tipo == Tipo::Casamento; }

// recorte:inicio classe-do-operador
ClasseDeOperador classeDoOperador(const TipoDeToken operador) {
    switch (operador) {
        case TipoDeToken::IgualIgual:
        case TipoDeToken::Diferente:
            return ClasseDeOperador::Igualdade;
        case TipoDeToken::Menor:
        case TipoDeToken::Maior:
        case TipoDeToken::MenorOuIgual:
        case TipoDeToken::MaiorOuIgual:
            return ClasseDeOperador::Ordem;
        default:
            return ClasseDeOperador::NaoComparador;
    }
}
// recorte:fim classe-do-operador

std::string textoDoOperador(const TipoDeToken operador) {
    switch (operador) {
        case TipoDeToken::IgualIgual:
            return "==";
        case TipoDeToken::Diferente:
            return "!=";
        case TipoDeToken::Menor:
            return "<";
        case TipoDeToken::Maior:
            return ">";
        case TipoDeToken::MenorOuIgual:
            return "<=";
        case TipoDeToken::MaiorOuIgual:
            return ">=";
        case TipoDeToken::OperadorAnd:
            return "and";
        case TipoDeToken::OperadorOr:
            return "or";
        default:
            return "?";
    }
}

// A TABELA. Quatro linhas, e elas são a especificação inteira das comparações da
// linguagem — o que se lê aqui é o que `docs/12_semantica.md` diz em prosa, sem
// intermediário. Repare no que NÃO está escrito: não há linha para número contra
// texto, e não há linha de ordem sobre texto. A ausência é a regra.
const std::vector<RegraDeComparacao>& regrasDeComparacao() {
    static const std::vector<RegraDeComparacao> regras = {
        {ClasseDeOperador::Ordem, Tipo::Numero, Tipo::Numero, Tipo::Logico},
        {ClasseDeOperador::Igualdade, Tipo::Numero, Tipo::Numero, Tipo::Logico},
        {ClasseDeOperador::Igualdade, Tipo::Texto, Tipo::Texto, Tipo::Logico},
    };
    return regras;
}

Veredito verificarComparacao(const Tipo esquerda, const TipoDeToken operador,
                             const Tipo direita) {
    Veredito veredito;

    // Silêncio deliberado sobre o que já foi recusado. Não é tolerância: é a
    // diferença entre relatar um defeito e relatar as consequências dele.
    if (esquerda == Tipo::Indefinido || direita == Tipo::Indefinido) {
        veredito.valida = true;
        veredito.resultado = Tipo::Logico;
        return veredito;
    }

    const ClasseDeOperador classe = classeDoOperador(operador);
    const Tipo normalEsquerda = normalizar(esquerda);
    const Tipo normalDireita = normalizar(direita);

    for (const RegraDeComparacao& regra : regrasDeComparacao()) {
        if (regra.classe == classe && regra.esquerda == normalEsquerda &&
            regra.direita == normalDireita) {
            veredito.valida = true;
            veredito.resultado = regra.resultado;
            return veredito;
        }
    }

    veredito.valida = false;
    veredito.resultado = Tipo::Indefinido;
    veredito.encontrado = nomeDoTipoDeValor(esquerda) + " " + textoDoOperador(operador) + " " +
                          nomeDoTipoDeValor(direita);

    // A explicação é escolhida pelo motivo da recusa, e não pela conveniência de
    // ter uma frase só. São três motivos distintos, e quem lê precisa saber qual
    // deles ocorreu para saber o que corrigir.
    if (classe == ClasseDeOperador::Ordem && ehTextual(normalEsquerda) &&
        ehTextual(normalDireita)) {
        veredito.esperado =
            "comparacao de ordem entre numeros; sobre texto a linguagem so compara igualdade";
    } else if (normalEsquerda == Tipo::Logico || normalDireita == Tipo::Logico) {
        veredito.esperado = "operandos comparaveis; uma condicao nao se compara com nada";
    } else {
        veredito.esperado = "os dois lados do mesmo tipo: numero com numero, ou texto com texto";
    }
    return veredito;
}

Veredito verificarConectivo(const TipoDeToken conectivo, const Tipo esquerda,
                            const Tipo direita) {
    Veredito veredito;
    veredito.resultado = Tipo::Logico;

    const bool esquerdaOk = esquerda == Tipo::Logico || esquerda == Tipo::Indefinido;
    const bool direitaOk = direita == Tipo::Logico || direita == Tipo::Indefinido;
    if (esquerdaOk && direitaOk) {
        veredito.valida = true;
        return veredito;
    }

    veredito.valida = false;
    veredito.esperado = "uma condicao dos dois lados de `" + textoDoOperador(conectivo) + "`";
    const Tipo culpado = esquerdaOk ? direita : esquerda;
    const std::string lado = esquerdaOk ? "a direita" : "a esquerda";
    veredito.encontrado = lado + " ha " + nomeDoTipoDeValor(culpado) +
                          ", que nao e uma condicao; talvez falte a comparacao";
    return veredito;
}

Contencao linguagemContida(const Afd& contida, const Afd& continente) {
    Contencao veredito;

    const std::size_t quantidadeContida = contida.quantidadeDeEstados();
    const std::size_t quantidadeContinente = continente.quantidadeDeEstados();

    // Máquina sem alfabeto é a máquina que a construção devolve quando falha, e
    // ela reconhece a linguagem vazia — o que faria a contenção sair "verdadeira"
    // por um motivo que não é o pretendido. Recusar-se a decidir é a resposta
    // honesta, e quem chama já sabe distinguir "não" de "não sei".
    if (quantidadeContida == 0 || quantidadeContinente == 0 || contida.alfabeto().empty() ||
        continente.alfabeto().empty()) {
        return veredito;  // indecidida
    }
    veredito.decidida = true;

    const std::string alfabeto = unirAlfabetos(contida.alfabeto(), continente.alfabeto());
    const std::size_t total = quantidadeContida * quantidadeContinente;

    std::vector<char> visitado(total, 0);
    std::vector<std::size_t> anterior(total, kSemPar);
    std::vector<char> simboloDeEntrada(total, '\0');

    const std::size_t inicial =
        contida.estadoInicial() * quantidadeContinente + continente.estadoInicial();
    visitado[inicial] = 1;

    std::vector<std::size_t> fila;
    fila.push_back(inicial);

    // A busca percorre PARES de estados. Um par alcançável em que a primeira
    // máquina aceita e a segunda não é a prova de que a contenção falha, e o
    // caminho até ele é a cadeia que testemunha a falha.
    for (std::size_t indice = 0; indice < fila.size(); ++indice) {
        const std::size_t par = fila[indice];
        const Estado estadoContida = par / quantidadeContinente;
        const Estado estadoContinente = par % quantidadeContinente;

        if (contida.ehDeAceitacao(estadoContida) && !continente.ehDeAceitacao(estadoContinente)) {
            veredito.contida = false;
            veredito.testemunha = reconstruirCadeia(par, anterior, simboloDeEntrada);
            return veredito;
        }

        for (const char simbolo : alfabeto) {
            const std::size_t proximo =
                contida.transicao(estadoContida, simbolo) * quantidadeContinente +
                continente.transicao(estadoContinente, simbolo);
            if (visitado[proximo] == 0) {
                visitado[proximo] = 1;
                anterior[proximo] = par;
                simboloDeEntrada[proximo] = simbolo;
                fila.push_back(proximo);
            }
        }
    }

    veredito.contida = true;
    return veredito;
}

std::string corpoDaExpressao(const std::string& lexemaEntreBarras) {
    if (lexemaEntreBarras.size() < 2) {
        return lexemaEntreBarras;
    }
    return lexemaEntreBarras.substr(1, lexemaEntreBarras.size() - 2);
}

PadraoDoUsuario compilarExpressaoDePadrao(const std::string& corpo) {
    PadraoDoUsuario compilado;

    const Resultado leitura = analisarExpressao(corpo);
    if (!leitura.ok) {
        compilado.ok = false;
        compilado.erro = "posicao " + std::to_string(leitura.erro.posicao + 1) + ": " +
                         leitura.erro.mensagem;
        return compilado;
    }

    // O caminho é o mesmo dos primeiros capítulos, sem atalho: árvore, autômato
    // de Thompson, determinização, minimização. Reusar o caminho inteiro é o que
    // garante que a máquina consultada aqui é a MESMA que a geração de código vai
    // emitir adiante — duas construções diferentes do mesmo padrão seriam duas
    // linguagens diferentes, e a verificação estaria falando da errada.
    const Afn afn = construirThompson(leitura.arvore);
    const std::string alfabeto = alfabetoDoAfn(afn);
    const ResultadoDaDeterminizacao determinizado = determinizar(afn, alfabeto);
    compilado.afd = minimizar(determinizado.afd).afd;
    compilado.ok = true;
    return compilado;
}

const std::string& expressaoDoFormatoNumerico() {
    // A MESMA expressão que descreve o literal numérico da linguagem. Escrevê-la
    // de novo, ligeiramente diferente, produziria uma linguagem em que `10.5` é
    // literal válido e não é número — que é o tipo de incoerência que ninguém
    // encontra lendo o código.
    static const std::string expressao = "-?[0-9]+(\\.[0-9]+)?";
    return expressao;
}

const Afd& afdDoFormatoNumerico() {
    static const Afd maquina = compilarExpressaoDePadrao(expressaoDoFormatoNumerico()).afd;
    return maquina;
}

}  // namespace peneira

Cumpri a exigência da tarefa ao pé da letra, e ela mudou o resultado. Antes de escrever uma linha do verificador, escrevi a especificação de significado da linguagem — um documento curto, no próprio pacote do projeto de referência, que diz quais são os tipos, o que cada comparação aceita, o que value exige e quais são as condições de invalidez, uma a uma. Só então implementei. O efeito foi que três regras mudaram enquanto eu as escrevia, e mudaram porque escrever “número compara com número, texto compara com texto” obriga a responder o que acontece quando os dois lados diferem, enquanto implementar permite não responder — o else cuida disso em silêncio, e o comportamento passa a ser o que sobrou.

A regra virou tabela, e não uma cadeia de condicionais. A diferença é operativa. Uma cadeia de condicionais responde “isto vale?” e não responde “o que vale?”, de modo que a única maneira de saber o que a linguagem aceita é ler o verificador inteiro e reconstruir a regra de cabeça. Com a tabela, a especificação e o código são legíveis lado a lado sem tradução, e acrescentar um tipo é acrescentar linhas. Repare no que a tabela não contém, porque a ausência é a regra: não há linha para número contra texto, e não há linha de ordem sobre texto.

A segunda ausência foi a decisão mais discutível do módulo, e ela é de projeto de linguagem, não de implementação. Ordenar texto exige uma regra de colação — maiúsculas antes ou depois, acentos onde, qual alfabeto —, e a Peneira não tem motivo para escolher uma. Escolher em silêncio produziria resultado que varia com a máquina, que é o pior defeito possível numa ferramenta cuja função é dizer o que casou. Recusar < sobre texto é menos conveniente e é honesto.

O tipo que distingue esta linguagem de um exercício genérico de tipos é o casamento: o trecho da entrada que um padrão reconheceu. Ele não é texto. O que se sabe sobre ele em tempo de compilação é qual padrão o produziu, e é daí que sai a verificação de value. A linguagem tem uma única promoção implícita, casamento para texto, declarada num lugar só — promoção implícita espalhada pelo verificador é a origem clássica do sistema de tipos que ninguém consegue descrever, porque cada ponto de conversão acrescenta uma regra que não está escrita em parte alguma.

E aqui está a peça de que mais me orgulho neste módulo, porque ela não custou nada e não poderia existir em outra disciplina. value(x) exige que o padrão que ligou x só case números. A linguagem não tem declaração de tipo de padrão, e acrescentar sintaxe para isso seria mudar a linguagem para caber na implementação. Então a resposta é decidida. Se L(p) é a linguagem do padrão e L(\mathrm{num}) a do formato numérico, a pergunta é se L(p) \subseteq L(\mathrm{num}) — e a contenção entre linguagens regulares é decidível, com o instrumento já construído nos primeiros capítulos. Percorre-se o produto dos dois autômatos determinísticos procurando um par de estados alcançável em que o primeiro aceite e o segundo recuse.

O que essa busca devolve quando a resposta é “não” vale tanto quanto a resposta. Como o percurso é em largura, o caminho até o par encontrado é a cadeia mais curta que distingue as duas linguagens, e ela vai para a mensagem: o padrão de endereço casa .@a.a, que não é número. A recusa deixa de ser uma opinião do verificador e passa a ser uma demonstração, com o contraexemplo em mãos. É o tipo de coisa que a teoria de autômatos costuma prometer no primeiro terço de um curso e raramente cobra depois; aqui ela paga.

Repare na cadeia que a busca escolheu, porque ela ensina mais do que uma cadeia bonita ensinaria. É .@a.a, começando por um ponto, e não a@a.a, que seria o endereço mínimo que um humano escreveria. A busca não procura exemplos plausíveis — procura o menor contraexemplo, e a classe de símbolos do padrão de endereço admite ponto na primeira posição. Além de provar que o padrão não é numérico, a cadeia revela de graça que ele aceita endereços que ninguém pretendia aceitar. É um efeito colateral do método, e é o tipo de informação que uma verificação por amostragem de casos jamais devolveria.

Há um limite, e ele está declarado no código em vez de escondido: o coringa é interpretado sobre o alfabeto dos símbolos escritos na própria expressão, de modo que um padrão que o use pode ser classificado como numérico sem o ser sobre um alfabeto maior. É restrição da construção do autômato e não desta verificação, e está registrada no ponto em que produz consequência visível.

Onde é fácil errar. Deixar a promoção de casamento para texto acontecer em dois lugares. Basta que a comparação normalize e a emissão não, e a linguagem passa a aceitar um casamento onde espera texto em metade dos contextos. Como verificar que está correta: escreva a tabela de compatibilidade em papel, sem olhar o código, e depois confira linha a linha. Se você não conseguir escrevê-la sem consultar a implementação, a especificação não existe — existe uma descrição do que o código faz.

1.4 Tarefa 3: Produzir mensagem específica para cada invalidez

O que a tarefa pede

Fazer cada condição de invalidez prevista pela linguagem produzir uma mensagem específica, dizendo o que se esperava e o que se encontrou. Uma mensagem genérica para dez situações diferentes custa menos para escrever e devolve ao usuário o trabalho de descobrir qual das dez ocorreu. Verificar também o caso oposto: uma descrição inteiramente válida precisa atravessar sem nenhum falso alarme.

12_sema.h
// 12_sema.h — A travessia que verifica o que a sintaxe não captura.
//
// O QUE ESTA FASE ACRESCENTA, e que nenhuma anterior podia acrescentar: a
// descrição `on emial(e) => emit("contato", e);` está sintaticamente perfeita. A
// gramática não tem como saber que `emial` nunca foi declarado, porque a
// gramática fala de FORMA e isto é uma questão de CONTEXTO. Toda a fase existe
// nesse vão.
//
// AS CONDIÇÕES DE INVALIDEZ SÃO ENUMERADAS, e a enumeração é a decisão de projeto
// que sustenta a terceira tarefa. Uma mensagem genérica para catorze situações
// custa menos para escrever e devolve a quem lê o trabalho de descobrir qual das
// catorze ocorreu. Com o enumerado, cada condição tem nome, mensagem própria e um
// ponto único no código onde ela é emitida — e a bateria de testes pode exigir
// que TODAS apareçam sobre a descrição defeituosa de referência, que é o que
// impede uma delas de silenciar sem ninguém notar.
//
// SEVERIDADE NÃO É DECORAÇÃO. Um padrão declarado e nunca usado não invalida a
// descrição: é um aviso. Um padrão citado e nunca declarado invalida: é um erro.
// Misturar os dois obrigaria a escolher entre recusar descrições legítimas e
// deixar defeitos passarem — e a primeira alternativa é a pior das duas, porque
// ensina quem usa o sistema a ignorar o que ele diz.
//
// O TRAÇO DE ATRIBUTOS não é um brinquedo ao lado da verificação: é a própria
// verificação, registrada. Cada nó recebe de cima o ambiente em que será
// interpretado — atributo HERDADO — e devolve para cima o seu tipo — atributo
// SINTETIZADO. A ordem em que os passos são registrados é a ordem de avaliação, e
// ela é pós-ordem por necessidade, não por gosto: o tipo de uma comparação não
// existe antes de os dois lados terem o seu.

#ifndef PENEIRA_12_SEMA_H
#define PENEIRA_12_SEMA_H

#include <cstddef>
#include <string>
#include <vector>

#include "10_ast.h"
#include "12_symtab.h"
#include "12_tipos.h"

namespace peneira {

enum class Severidade {
    Erro,
    Aviso,
};

// As condições previstas pela linguagem, uma a uma. Acrescentar uma condição é
// acrescentar um valor aqui, e o compilador então aponta todos os lugares que
// precisam saber dela.
enum class Falta {
    PadraoMalFormado,
    PadraoRedeclarado,
    PadraoNaoDeclarado,
    PadraoNaoUsado,
    LigacaoOcultaPadrao,
    LigacaoNaoUsada,
    NomeNaoLigado,
    ValorDeNaoLigacao,
    ValorDePadraoNaoNumerico,
    ComparacaoIncompativel,
    OperandoLogicoInvalido,
    CondicaoNaoLogica,
    EmissaoDeCondicao,
    SemBlocoDeRegras,
};

std::string nomeDaFalta(Falta falta);

struct Diagnostico {
    Falta falta = Falta::PadraoNaoDeclarado;
    Severidade severidade = Severidade::Erro;
    Posicao posicao;
    std::string mensagem;  // o que se esperava e o que se encontrou
    std::string remedio;   // opcional: o próximo passo de quem lê
};

// Um passo da avaliação dos atributos sobre a árvore de uma ação.
struct PassoDeAtributo {
    std::size_t ordem = 0;
    std::size_t profundidade = 0;
    std::string no;        // que nó da árvore
    std::string herdado;   // o ambiente entregue de cima
    Tipo sintetizado = Tipo::Indefinido;  // o tipo devolvido para cima
};

struct ResultadoSemantico {
    std::vector<Diagnostico> diagnosticos;
    std::vector<PassoDeAtributo> traco;
    TabelaDeNomes tabela;

    bool ok() const;  // nenhum diagnóstico de severidade `Erro`
    std::size_t erros() const;
    std::size_t avisos() const;
    bool contem(Falta falta) const;
};

class AnalisadorSemantico {
public:
    explicit AnalisadorSemantico(const Programa& programa);

    ResultadoSemantico analisar();

private:
    void declararPadroes();
    void verificarAcao(const Acao& acao);
    Tipo tipoDe(const Expressao& expressao, std::size_t profundidade);
    void relatarNaoUsados();

    void registrar(Falta falta, Severidade severidade, const Posicao& posicao,
                   const std::string& mensagem, const std::string& remedio);
    void anotar(std::size_t profundidade, const std::string& no, Tipo sintetizado);
    std::string ambienteCorrente() const;

    const Programa& programa_;
    ResultadoSemantico resultado_;
    TabelaDeNomes tabela_;
    std::string ligacaoCorrente_;
    std::string padraoCorrente_;
};

std::string formatarDiagnostico(const std::string& texto, const Diagnostico& diagnostico);
std::string formatarTracoDeAtributos(const std::vector<PassoDeAtributo>& traco);

}  // namespace peneira

#endif  // PENEIRA_12_SEMA_H
12_sema.cpp
#include "12_sema.h"

#include <sstream>

namespace peneira {
namespace {

// A linha inteira em que o deslocamento cai, para que a mensagem mostre o texto e
// não apenas coordenadas. É a terceira cópia desta função no sistema — o
// reconhecedor de símbolos e o analisador sintático têm a sua. Promovê-la a peça
// comum obrigaria a editar dois módulos já fechados e verificados, e o custo
// dessa edição é maior do que o de doze linhas repetidas; o registro fica aqui
// para que a decisão seja visível em vez de acidental.
std::string linhaDe(const std::string& texto, const std::size_t deslocamento) {
    if (texto.empty()) {
        return {};
    }
    const std::size_t posicao = deslocamento < texto.size() ? deslocamento : texto.size() - 1;
    std::size_t inicio = posicao;
    while (inicio > 0 && texto[inicio - 1] != '\n') {
        --inicio;
    }
    std::size_t fim = posicao;
    while (fim < texto.size() && texto[fim] != '\n') {
        ++fim;
    }
    return texto.substr(inicio, fim - inicio);
}

std::string preencher(std::string texto, const std::size_t largura) {
    if (texto.size() < largura) {
        texto.append(largura - texto.size(), ' ');
    }
    return texto;
}

}  // namespace

std::string nomeDaFalta(const Falta falta) {
    switch (falta) {
        case Falta::PadraoMalFormado:
            return "padrao mal formado";
        case Falta::PadraoRedeclarado:
            return "padrao redeclarado";
        case Falta::PadraoNaoDeclarado:
            return "padrao nao declarado";
        case Falta::PadraoNaoUsado:
            return "padrao nao usado";
        case Falta::LigacaoOcultaPadrao:
            return "ligacao oculta padrao";
        case Falta::LigacaoNaoUsada:
            return "ligacao nao usada";
        case Falta::NomeNaoLigado:
            return "nome nao ligado";
        case Falta::ValorDeNaoLigacao:
            return "value sobre o que nao e ligacao";
        case Falta::ValorDePadraoNaoNumerico:
            return "value sobre padrao nao numerico";
        case Falta::ComparacaoIncompativel:
            return "comparacao incompativel";
        case Falta::OperandoLogicoInvalido:
            return "operando de conectivo nao e condicao";
        case Falta::CondicaoNaoLogica:
            return "condicao nao e logica";
        case Falta::EmissaoDeCondicao:
            return "emissao de condicao";
        case Falta::SemBlocoDeRegras:
            return "descricao sem bloco de regras";
    }
    return "falta desconhecida";
}

bool ResultadoSemantico::ok() const { return erros() == 0; }

std::size_t ResultadoSemantico::erros() const {
    std::size_t total = 0;
    for (const Diagnostico& diagnostico : diagnosticos) {
        if (diagnostico.severidade == Severidade::Erro) {
            ++total;
        }
    }
    return total;
}

std::size_t ResultadoSemantico::avisos() const {
    return diagnosticos.size() - erros();
}

bool ResultadoSemantico::contem(const Falta falta) const {
    for (const Diagnostico& diagnostico : diagnosticos) {
        if (diagnostico.falta == falta) {
            return true;
        }
    }
    return false;
}

AnalisadorSemantico::AnalisadorSemantico(const Programa& programa) : programa_(programa) {}

void AnalisadorSemantico::registrar(const Falta falta, const Severidade severidade,
                                    const Posicao& posicao, const std::string& mensagem,
                                    const std::string& remedio) {
    Diagnostico diagnostico;
    diagnostico.falta = falta;
    diagnostico.severidade = severidade;
    diagnostico.posicao = posicao;
    diagnostico.mensagem = mensagem;
    diagnostico.remedio = remedio;
    resultado_.diagnosticos.push_back(std::move(diagnostico));
}

std::string AnalisadorSemantico::ambienteCorrente() const {
    if (ligacaoCorrente_.empty()) {
        return "escopo 0: apenas os padroes";
    }
    return "escopo 1: " + ligacaoCorrente_ + " : casamento de " + padraoCorrente_;
}

void AnalisadorSemantico::anotar(const std::size_t profundidade, const std::string& no,
                                 const Tipo sintetizado) {
    PassoDeAtributo passo;
    passo.ordem = resultado_.traco.size() + 1;
    passo.profundidade = profundidade;
    passo.no = no;
    passo.herdado = ambienteCorrente();
    passo.sintetizado = sintetizado;
    resultado_.traco.push_back(std::move(passo));
}

void AnalisadorSemantico::declararPadroes() {
    for (const DeclaracaoDePadrao& declaracao : programa_.padroes) {
        Simbolo simbolo;
        simbolo.nome = declaracao.nome;
        simbolo.especie = EspecieDeSimbolo::Padrao;
        simbolo.tipo = Tipo::Casamento;
        simbolo.declaracao = declaracao.posicao;
        simbolo.expressao = corpoDaExpressao(declaracao.expressao);

        // A expressão do padrão chega aqui SEM nunca ter sido verificada: para o
        // reconhecedor de símbolos ela é o trecho entre barras, e nada mais. É
        // esta fase que descobre se ela é uma expressão regular — e é por isso
        // que a recusa de um padrão mal formado é semântica e não léxica.
        const PadraoDoUsuario compilado = compilarExpressaoDePadrao(simbolo.expressao);
        simbolo.compilou = compilado.ok;
        if (!compilado.ok) {
            registrar(Falta::PadraoMalFormado, Severidade::Erro, declaracao.posicao,
                      "a expressao do padrao `" + declaracao.nome +
                          "` nao e uma expressao regular valida — " + compilado.erro,
                      "as construcoes aceitas sao concatenacao, `|`, `*`, `+`, `?`, classes "
                      "entre colchetes, grupos entre parenteses e `\\` para escapar");
        } else {
            const Contencao contencao =
                linguagemContida(compilado.afd, afdDoFormatoNumerico());
            simbolo.decidido = contencao.decidida;
            simbolo.numerico = contencao.contida;
            simbolo.testemunha = contencao.testemunha;
        }

        if (const Simbolo* anterior = tabela_.declarar(simbolo)) {
            registrar(Falta::PadraoRedeclarado, Severidade::Erro, declaracao.posicao,
                      "o padrao `" + declaracao.nome + "` ja foi declarado na linha " +
                          std::to_string(anterior->declaracao.linha) + ", coluna " +
                          std::to_string(anterior->declaracao.coluna),
                      "apague uma das duas declaracoes ou de outro nome a esta");
        }
    }
}

Tipo AnalisadorSemantico::tipoDe(const Expressao& expressao, const std::size_t profundidade) {
    switch (expressao.tipo) {
        case TipoDeExpressao::Numero: {
            anotar(profundidade, "numero " + expressao.lexema, Tipo::Numero);
            return Tipo::Numero;
        }

        case TipoDeExpressao::Texto: {
            anotar(profundidade, "texto " + expressao.lexema, Tipo::Texto);
            return Tipo::Texto;
        }

        case TipoDeExpressao::Nome: {
            Tipo tipo = Tipo::Indefinido;
            Simbolo* simbolo = tabela_.consultar(expressao.lexema);
            if (simbolo == nullptr || simbolo->especie != EspecieDeSimbolo::Ligacao) {
                const std::string parecido =
                    tabela_.nomeMaisParecido(expressao.lexema, EspecieDeSimbolo::Ligacao);
                registrar(Falta::NomeNaoLigado, Severidade::Erro, expressao.posicao,
                          "o nome `" + expressao.lexema +
                              "` nao esta ligado nesta acao; a ligacao desta acao e `" +
                              ligacaoCorrente_ + "`",
                          parecido.empty() ? std::string{} : "talvez `" + parecido + "`");
            } else {
                ++simbolo->usos;
                tipo = simbolo->tipo;
            }
            anotar(profundidade, "nome " + expressao.lexema, tipo);
            return tipo;
        }

        case TipoDeExpressao::ValorDe: {
            // `value` devolve numero mesmo quando o argumento e recusado: propagar
            // `Indefinido` daqui faria a comparacao ao redor tambem ser recusada,
            // e o mesmo defeito sairia duas vezes em lugares diferentes.
            Simbolo* simbolo = tabela_.consultar(expressao.lexema);
            if (simbolo == nullptr) {
                const std::string parecido =
                    tabela_.nomeMaisParecido(expressao.lexema, EspecieDeSimbolo::Ligacao);
                registrar(Falta::NomeNaoLigado, Severidade::Erro, expressao.posicao,
                          "o nome `" + expressao.lexema +
                              "` nao esta ligado nesta acao; a ligacao desta acao e `" +
                              ligacaoCorrente_ + "`",
                          parecido.empty() ? std::string{} : "talvez `" + parecido + "`");
            } else if (simbolo->especie != EspecieDeSimbolo::Ligacao) {
                registrar(Falta::ValorDeNaoLigacao, Severidade::Erro, expressao.posicao,
                          "`value` converte o trecho casado por uma ligacao, e `" +
                              expressao.lexema + "` e um padrao, nao uma ligacao",
                          "escreva `value(" + ligacaoCorrente_ + ")`, que e a ligacao desta acao");
            } else {
                ++simbolo->usos;
                if (simbolo->compilou && simbolo->decidido && !simbolo->numerico) {
                    registrar(Falta::ValorDePadraoNaoNumerico, Severidade::Erro,
                              expressao.posicao,
                              "`value(" + expressao.lexema +
                                  ")` exige um padrao que so case numeros, e `" +
                                  simbolo->padraoDeOrigem + "` casa \"" + simbolo->testemunha +
                                  "\", que nao e numero",
                              "compare o trecho como texto, ou restrinja a expressao do padrao "
                              "`" + simbolo->padraoDeOrigem + "`");
                }
            }
            anotar(profundidade, "value(" + expressao.lexema + ")", Tipo::Numero);
            return Tipo::Numero;
        }

        case TipoDeExpressao::Comparacao: {
            const Tipo esquerda =
                expressao.esquerda ? tipoDe(*expressao.esquerda, profundidade + 1)
                                   : Tipo::Indefinido;
            const Tipo direita =
                expressao.direita ? tipoDe(*expressao.direita, profundidade + 1)
                                  : Tipo::Indefinido;
            const Veredito veredito =
                verificarComparacao(esquerda, expressao.operador, direita);
            if (!veredito.valida) {
                registrar(Falta::ComparacaoIncompativel, Severidade::Erro, expressao.posicao,
                          "esperava-se " + veredito.esperado + ", e encontrou-se " +
                              veredito.encontrado,
                          "");
            }
            anotar(profundidade, "comparacao " + textoDoOperador(expressao.operador),
                   Tipo::Logico);
            return Tipo::Logico;
        }

        case TipoDeExpressao::Conjuncao:
        case TipoDeExpressao::Disjuncao: {
            const TipoDeToken conectivo = expressao.tipo == TipoDeExpressao::Conjuncao
                                              ? TipoDeToken::OperadorAnd
                                              : TipoDeToken::OperadorOr;
            const Tipo esquerda =
                expressao.esquerda ? tipoDe(*expressao.esquerda, profundidade + 1)
                                   : Tipo::Indefinido;
            const Tipo direita =
                expressao.direita ? tipoDe(*expressao.direita, profundidade + 1)
                                  : Tipo::Indefinido;
            const Veredito veredito = verificarConectivo(conectivo, esquerda, direita);
            if (!veredito.valida) {
                registrar(Falta::OperandoLogicoInvalido, Severidade::Erro, expressao.posicao,
                          "esperava-se " + veredito.esperado + ", e " + veredito.encontrado, "");
            }
            anotar(profundidade, "conectivo " + textoDoOperador(conectivo), Tipo::Logico);
            return Tipo::Logico;
        }
    }
    return Tipo::Indefinido;
}

void AnalisadorSemantico::verificarAcao(const Acao& acao) {
    // O que se sabe do padrão é copiado para variáveis locais AGORA, antes de o
    // escopo da ação ser aberto. Não é preciosismo: guardar o ponteiro e lê-lo
    // depois amarraria a correção deste trecho a uma garantia de estabilidade de
    // endereços que a assinatura da tabela não promete.
    bool padraoConhecido = false;
    Simbolo herdado;
    if (Simbolo* padrao = tabela_.consultar(acao.padrao)) {
        if (padrao->especie == EspecieDeSimbolo::Padrao) {
            padraoConhecido = true;
            ++padrao->usos;
            herdado = *padrao;
        }
    }
    if (!padraoConhecido) {
        const std::string parecido =
            tabela_.nomeMaisParecido(acao.padrao, EspecieDeSimbolo::Padrao);
        registrar(Falta::PadraoNaoDeclarado, Severidade::Erro, acao.posicaoDoPadrao,
                  "o padrao `" + acao.padrao +
                      "` nao foi declarado; `on` so cita padrao declarado antes do bloco",
                  parecido.empty() ? std::string{} : "talvez `" + parecido + "`");
    }

    // A ligação vive só dentro desta ação, e o escopo é o que garante isso: na
    // ação seguinte, o mesmo nome pode significar outra coisa — ou não significar
    // nada, que é o caso interessante, porque é o que uma tabela plana não pega.
    tabela_.abrirEscopo();
    padraoCorrente_ = acao.padrao;
    ligacaoCorrente_ = acao.ligacao;

    if (const Simbolo* homonimo = tabela_.consultar(acao.ligacao)) {
        if (homonimo->especie == EspecieDeSimbolo::Padrao) {
            registrar(Falta::LigacaoOcultaPadrao, Severidade::Aviso, acao.posicao,
                      "a ligacao `" + acao.ligacao + "` oculta o padrao homonimo dentro desta "
                      "acao, e o nome passa a significar duas coisas na mesma descricao",
                      "de outro nome a ligacao");
        }
    }

    Simbolo ligacao;
    ligacao.nome = acao.ligacao;
    ligacao.especie = EspecieDeSimbolo::Ligacao;
    ligacao.tipo = Tipo::Casamento;
    ligacao.declaracao = acao.posicao;
    ligacao.padraoDeOrigem = acao.padrao;
    if (padraoConhecido) {
        ligacao.compilou = herdado.compilou;
        ligacao.decidido = herdado.decidido;
        ligacao.numerico = herdado.numerico;
        ligacao.testemunha = herdado.testemunha;
    }
    tabela_.declarar(ligacao);

    if (acao.condicao) {
        const Tipo tipo = tipoDe(*acao.condicao, 0);
        if (tipo != Tipo::Logico && tipo != Tipo::Indefinido) {
            registrar(Falta::CondicaoNaoLogica, Severidade::Erro, acao.condicao->posicao,
                      "a condicao de `where` precisa ser logica, e esta e " +
                          nomeDoTipoDeValor(tipo),
                      "talvez falte a comparacao que transforma este valor numa condicao");
        }
    }

    if (acao.valor) {
        const Tipo tipo = tipoDe(*acao.valor, 0);
        if (tipo == Tipo::Logico) {
            registrar(Falta::EmissaoDeCondicao, Severidade::Erro, acao.valor->posicao,
                      "o segundo argumento de `emit` e o valor a emitir, e uma condicao nao e "
                      "um valor emissivel",
                      "emita o casamento ou um numero; a condicao pertence ao `where`");
        }
    }

    tabela_.fecharEscopo();
    ligacaoCorrente_.clear();
    padraoCorrente_.clear();
}

void AnalisadorSemantico::relatarNaoUsados() {
    // A metade fácil de esquecer da verificação: o que foi declarado e nunca
    // consultado. Não invalida nada, e por isso é aviso — mas quase sempre é um
    // nome escrito errado no lugar de uso, e sinalizá-lo poupa a busca.
    for (const Simbolo& simbolo : tabela_.historico()) {
        if (simbolo.usos > 0) {
            continue;
        }
        if (simbolo.especie == EspecieDeSimbolo::Padrao) {
            registrar(Falta::PadraoNaoUsado, Severidade::Aviso, simbolo.declaracao,
                      "o padrao `" + simbolo.nome +
                          "` foi declarado e nenhuma acao o cita, entao ele nao produz saida "
                          "alguma",
                      "cite-o num `on` ou apague a declaracao");
        } else {
            registrar(Falta::LigacaoNaoUsada, Severidade::Aviso, simbolo.declaracao,
                      "a ligacao `" + simbolo.nome +
                          "` foi criada e nao e usada nesta acao, que emite sempre a mesma "
                          "coisa independentemente do que casou",
                      "use `" + simbolo.nome + "` no `where` ou no `emit`, se era essa a "
                      "intencao");
        }
    }
}

ResultadoSemantico AnalisadorSemantico::analisar() {
    declararPadroes();

    if (!programa_.temBlocoDeRegras) {
        Posicao inicio;
        registrar(Falta::SemBlocoDeRegras, Severidade::Aviso, inicio,
                  "a descricao declara padroes e nenhum bloco `rule`, entao ela reconhece e nao "
                  "emite nada",
                  "acrescente um bloco `rule` com ao menos uma acao");
    }

    for (const Acao& acao : programa_.acoes) {
        verificarAcao(acao);
    }

    relatarNaoUsados();

    resultado_.tabela = tabela_;
    return resultado_;
}

std::string formatarDiagnostico(const std::string& texto, const Diagnostico& diagnostico) {
    std::ostringstream saida;
    const std::string rotulo =
        diagnostico.severidade == Severidade::Erro ? "erro semantico" : "aviso semantico";
    saida << rotulo << " em " << diagnostico.posicao.linha << ":" << diagnostico.posicao.coluna
          << " — " << diagnostico.mensagem << '\n';
    saida << "  " << linhaDe(texto, diagnostico.posicao.deslocamento) << '\n';
    saida << "  " << std::string(diagnostico.posicao.coluna - 1, ' ') << "^\n";
    if (!diagnostico.remedio.empty()) {
        saida << "  " << diagnostico.remedio << '\n';
    }
    return saida.str();
}

std::string formatarTracoDeAtributos(const std::vector<PassoDeAtributo>& traco) {
    std::ostringstream saida;
    saida << "  ordem  no                        herdado (ambiente)                  sintetizado\n";
    saida << "  -----  ------------------------  ----------------------------------  -----------\n";
    for (const PassoDeAtributo& passo : traco) {
        std::string no(passo.profundidade * 2, ' ');
        no += passo.no;
        saida << "  " << preencher(std::to_string(passo.ordem), 5) << "  " << preencher(no, 24)
              << "  " << preencher(passo.herdado, 34) << "  "
              << nomeDoTipoDeValor(passo.sintetizado) << '\n';
    }
    return saida.str();
}

}  // namespace peneira

A decisão que sustenta esta tarefa é enumerar as condições. Cada uma tem nome, mensagem própria e um ponto único no código onde é emitida, e são catorze. Enumerá-las é o que permite à bateria de testes exigir que todas apareçam sobre a descrição defeituosa de referência, e é isso que impede uma delas de silenciar sem ninguém notar. Sem o enumerado, uma condição que deixa de ser detectada não produz sintoma nenhum — o verificador continua compilando, continua recusando os outros casos, e o defeito só aparece quando alguém escrever exatamente aquele erro e não receber resposta.

A segunda decisão é separar severidade. Um padrão declarado e nunca citado não invalida a descrição: não produz saída alguma, quase sempre por descuido, e é aviso. Um padrão citado e nunca declarado invalida: é erro. Misturar os dois obriga a escolher entre recusar descrições legítimas e deixar defeitos passarem, e a primeira alternativa é a pior das duas, porque ensina quem usa o sistema a ignorar o que ele diz. A partir do instante em que o usuário aprende que o verificador reclama do que está certo, ele para de ler as reclamações — e a próxima, que estava certa, também não será lida.

A terceira decisão é o que fazer com o que já foi recusado. Um nome não ligado dentro de uma comparação produziria, sem cuidado, duas mensagens: a do nome e a da comparação incompatível, esta última apontando para um lugar em que não há defeito algum. O sistema de tipos tem, por isso, um valor que não é tipo do usuário e sim marcador de “aqui já houve recusa”, e toda regra o atravessa em silêncio. value sobre um argumento recusado devolve número do mesmo modo, e pela mesma razão. É a diferença entre relatar um defeito e relatar as consequências dele.

O que cada mensagem precisa dizer está no formato do diagnóstico, e é o que a tarefa pede: o que se esperava e o que se encontrou, com a linha do texto e o cursor sob a coluna, mais um próximo passo quando existe um. Comparar número com texto não devolve “erro de tipo”; devolve que se esperavam os dois lados do mesmo tipo e se encontrou numero > texto. O and com um casamento à esquerda não devolve “operando inválido”; devolve que se esperava uma condição dos dois lados e que à esquerda há um casamento, talvez faltando a comparação. A diferença entre as duas formas é o tempo que quem lê gasta antes de saber o que fazer, e esse tempo é pago por alguém que está tentando terminar um trabalho.

A metade oposta é a que quase ninguém testa, e a bateria a exige primeiro. A descrição de referência — a mesma que atravessa o sistema desde o primeiro capítulo — precisa produzir zero diagnósticos, e se algum dia produzir um, o teste reprova. O defeito que essa exigência previne é invisível de outro modo: um verificador que recusa tudo passa em toda bateria de casos inválidos, com nota máxima, e só falha no único caso que importa.

Onde é fácil errar. Escrever a mensagem no ponto de emissão, com o texto interpolado ali mesmo, espalhado por vinte lugares. Funciona no primeiro dia e envelhece mal: quando a linguagem mudar, metade das mensagens continuará descrevendo a versão antiga, e nada acusará. Como verificar que está correta: rode o verificador sobre a descrição defeituosa e leia as mensagens na ordem em que saem, fingindo não conhecer o sistema. Se em alguma delas você precisar abrir o código para entender o que fazer, aquela mensagem não está pronta.

1.5 Os atributos, e a ordem em que eles ficam prontos

Falta a parte da teoria do módulo que não cabe naturalmente em nenhuma das três tarefas, e que a solução cobre com código próprio: gramáticas de atributos e esquemas de tradução dirigidos pela sintaxe. O risco aqui é tratá-la como vocabulário a decorar — sintetizado sobe, herdado desce —, e a decisão que tomei para evitar isso foi não escrever demonstração nenhuma em separado. O verificador registra o que faz, e o registro é a demonstração.

Cada nó da árvore da condição recebe de cima o ambiente em que será interpretado — qual ligação existe, de que padrão ela vem — e devolve para cima o seu tipo. O primeiro é um atributo herdado: ele não depende do nó, depende de onde o nó está, e a ação inteira o entrega igual a toda a subárvore. O segundo é sintetizado: ele é computado dos filhos para o pai, e não existe antes de os filhos terem o seu. O traço impresso pela demonstração é uma tabela com as duas colunas, na ordem em que os passos aconteceram.

Essa ordem é o ponto, e ela é pós-ordem por necessidade e não por gosto. O tipo de uma comparação não existe antes de os dois lados terem o seu, então o nó da comparação só pode ser anotado depois de os filhos terem sido. O ambiente, ao contrário, está pronto antes de a descida começar — e é por isso que ele pode descer. Um esquema de tradução que precisasse de um atributo herdado computado a partir de irmãos à direita não caberia numa única passada descendente, e é exatamente aí que a escolha da estratégia de análise passa a restringir a tradução em vez de apenas precedê-la. Nesta linguagem isso não acontece, e vale dizer por quê em vez de deixar parecer sorte: o único atributo herdado é o ambiente, ele é constante dentro da ação, e a árvore de condição não tem construção que ligue nomes novos no meio dela.

Há um segundo lugar em que a herança de atributo aparece, e ele é mais concreto que o traço: a ligação criada por um on herda do padrão, no ato da declaração, a informação de numericidade que a verificação de value vai consultar depois. É atributo herdado no sentido literal, entre nós da árvore da ação, e a alternativa — consultar o padrão no momento do uso — está errada por uma razão que só o escopo revela.

1.6 O que este capítulo entrega ao arco seguinte

O sistema sai daqui com três coisas que não tinha ao entrar, e as três são consumidas adiante e não aqui. A primeira é a tabela de símbolos com os padrões classificados: a geração de código precisa saber, para cada padrão, qual autômato emitir, e o autômato já está construído e minimizado porque foi construído para responder a pergunta da numericidade. Compilar duas vezes o mesmo padrão, por caminhos diferentes, produziria duas linguagens ligeiramente diferentes com o mesmo nome — e a verificação estaria falando da errada.

A segunda é a garantia de que toda árvore que chega à geração de código tem significado. É uma garantia estreita e vale ter clareza sobre o tamanho dela: não diz que a descrição faz o que o autor queria, diz que ela não contém as catorze coisas que a linguagem prevê como impossíveis. O gerador pode, a partir daqui, assumir que todo nome consultado existe e que todo operador recebe operandos compatíveis, e essa suposição é o que lhe permite emitir código sem verificar nada — que é como deve ser, porque verificar duas vezes é ter duas respostas para manter em acordo.

A terceira é a mais barata de descrever e a que mais se nota em uso: o sistema passou a explicar as recusas. Até o capítulo anterior ele dizia onde o texto não obedecia à forma; agora diz por que ele não faz sentido, e diz de um jeito que aponta o próximo passo. É a primeira vez no percurso em que a qualidade do produto não se mede pelo que ele aceita, e sim pelo que ele consegue dizer sobre o que recusou.