Moacyr Francischetti Corrêa

1 Módulo 13: Projeto do Professor — Decidir o Formato Antes de Escrevê-lo

Este é o projeto de referência resolvido pelo professor: a mesma atividade que cada grupo vai executar neste módulo, feita por inteiro, com as decisões justificadas uma a uma. É o modelo do que o seu grupo entrega, não algo a copiar.

1.1 Visão Geral do Módulo 13

Este é o módulo de projeto no sentido mais literal do termo: quase todo o trabalho é decidir e documentar, e o código que resulta é consequência dessas decisões. O entregável principal não é um programa — é uma especificação, e o critério de qualidade dela é literal e será aplicado literalmente: outra pessoa consegue escrever um executor compatível lendo apenas ela?

A atividade tem três partes. Definir a representação intermediária que fica entre a análise e a síntese, com a justificativa da escolha. Especificar o formato do programa objeto e o modelo de execução que o interpreta. E implementar a tradução da árvore verificada para a representação escolhida.

Resolvi a atividade e vou começar pela parte incômoda, porque ela define o tom do módulo. O argumento clássico a favor de uma camada intermediária é combinatório: para m origens e n destinos, a tradução direta custa m \times n tradutores e a intermediária custa m + n. Calculei isso para o nosso caso e o resultado é constrangedor — temos uma origem e um destino, então direto custaria um tradutor e com a camada intermediária custa dois. O argumento que a literatura usa para justificar a camada, aplicado honestamente a este projeto, a condena.

A camada existe assim mesmo, e por outro motivo, que é o que este módulo descobriu: o curto-circuito dos conectivos lógicos exige desvio, desvio exige rótulo, e rótulo exige um endereço que só se conhece depois de gerar o que vem adiante. Misturar essa dificuldade com a emissão do objeto torna as duas difíceis. Separá-las é o que as torna tratáveis. A camada se paga em clareza, não em reaproveitamento — e dizer isso é mais útil ao grupo do que repetir o argumento combinatório onde ele não se aplica.

A segunda coisa que este módulo me obrigou a enfrentar foi uma divergência entre o design do artefato e o que o módulo seguinte exige. Está registrada na tarefa 3, com a decisão que tomei.

1.2 Tarefa 1: Justificar a camada, com números honestos

A atividade — justificar a existência de uma representação intermediária à luz das alternativas e das características da linguagem.

13_ri.h
#ifndef PENEIRA_13_RI_H
#define PENEIRA_13_RI_H

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

#include "10_ast.h"

namespace peneira {

// Representação intermediária da Peneira: código de três endereços.
//
// Por que existe uma camada aqui, em vez de a árvore verificada ir direto para
// o objeto. O argumento é combinatório e vale escrever com números: para
// suportar $m$ linguagens de origem e $n$ destinos, a tradução direta custa
// $m \times n$ tradutores; com uma representação intermediária no meio, custa
// $m + n$. Cinco por cinco: vinte e cinco contra dez.
//
// Nesta disciplina $m = n = 1$, e o argumento combinatório sozinho NÃO
// justificaria a camada — seria uma indireção paga sem retorno. O que a
// justifica aqui é outra coisa, e é honesto dizer qual: o curto-circuito dos
// conectivos lógicos exige desvios, e desvios exigem rótulos cujo destino só
// se conhece depois. Fazer isso direto sobre a árvore, durante a emissão do
// objeto, mistura duas dificuldades; separá-las é o que torna as duas
// tratáveis. A camada se paga em clareza, não em reaproveitamento.

enum class OpRI : std::uint8_t {
    Rotulo,        // marca um ponto do código; alvo de desvio
    CasamentoDe,   // t := o texto casado pela variável de ligação
    Valor,         // t := valor numérico de um casamento (o `value` da fonte)
    Constante,     // t := literal
    Comparacao,    // t := arg1 <op> arg2
    DesvioSeFalso, // se arg1 for falso, desvia
    Desvio,        // desvia incondicionalmente
    Emite,         // emite (rotulo, arg1)
};

const char* nomeDaOpRI(OpRI op) noexcept;

// Uma instrução de três endereços: um resultado e até dois operandos.
//
// `destino` é o índice da instrução alvo, para os desvios. Ele nasce com
// kDestinoPendente e é preenchido depois — é o campo que a técnica de
// preenchimento retroativo manipula.
inline constexpr std::size_t kDestinoPendente = static_cast<std::size_t>(-1);

struct InstrucaoRI {
    OpRI op = OpRI::Rotulo;
    std::string resultado;  // nome do temporário, ou do rótulo
    std::string arg1;
    std::string arg2;
    std::string operador;   // só para Comparacao
    std::size_t destino = kDestinoPendente;
};

// O código de uma regra: a condição (com desvios) seguida da emissão.
struct CodigoRI {
    std::vector<InstrucaoRI> instrucoes;
    std::string padrao;   // padrão que dispara a regra
    std::string ligacao;  // variável de ligação
};

std::string formatarRI(const CodigoRI& c);

// Tradutor de árvore verificada para código de três endereços.
//
// A técnica central é o **preenchimento retroativo**: ao traduzir uma condição,
// o desvio que sai quando ela é falsa precisa apontar para o fim da regra, e o
// fim da regra ainda não foi gerado. Em vez de fazer um segundo percurso para
// descobrir o endereço, a tradução guarda a lista das instruções incompletas e
// as preenche quando o endereço fica conhecido.
//
// É a mesma ideia do módulo 10 em outro disfarce: lá, a decisão sobre qual
// produção usar era adiada até haver informação; aqui, a decisão sobre para
// onde desviar.
class TradutorRI {
public:
    // Traduz uma ação verificada (nó Acao) para código de três endereços.
    CodigoRI traduzirAcao(const NoAst& acao);

    std::size_t temporariosUsados() const noexcept;

private:
    // Traduz uma expressão de valor e devolve o nome do temporário que a
    // contém.
    std::string traduzirValor(const NoAst& expr);

    // Traduz uma condição. Devolve as listas de instruções de desvio que ainda
    // precisam de destino: as que saem quando a condição é verdadeira e as que
    // saem quando é falsa.
    struct ListasDeDesvio {
        std::vector<std::size_t> verdadeiro;
        std::vector<std::size_t> falso;
    };
    ListasDeDesvio traduzirCondicao(const NoAst& cond);

    // Preenche o destino de todas as instruções da lista.
    void preencher(const std::vector<std::size_t>& lista, std::size_t destino);

    std::size_t emitir(InstrucaoRI instrucao);
    std::string novoTemporario();

    std::vector<InstrucaoRI> instrucoes_;
    std::size_t proximoTemporario_ = 0;
};

// Notação pós-fixada da mesma expressão.
//
// Está aqui porque é a forma intermediária mais simples que existe e serve de
// contraste: ela dispensa temporários e rótulos, porque a ordem de avaliação
// está na própria sequência. O preço é que ela não exprime desvio — e é
// exatamente por isso que ela não serve para o curto-circuito, o que fecha o
// argumento a favor do código de três endereços nesta linguagem.
std::string emPosFixada(const NoAst& expr);

// O argumento combinatório, calculado.
struct CustoDeTraducao {
    std::size_t origens = 0;
    std::size_t destinos = 0;
    std::size_t tradutoresDiretos = 0;      // origens * destinos
    std::size_t tradutoresComIntermediaria = 0;  // origens + destinos
};

CustoDeTraducao custoDeTraducao(std::size_t origens, std::size_t destinos) noexcept;

}  // namespace peneira

#endif  // PENEIRA_13_RI_H

A demonstração --porqueri calcula o argumento clássico em vez de enunciá-lo:

  origens | destinos | tradutores diretos | com intermediaria
  --------+----------+--------------------+------------------
      1   |     1    |          1         |          2
      2   |     2    |          4         |          4
      3   |     3    |          9         |          6
      5   |     5    |         25         |         10
     10   |    10    |        100         |         20

O ponto de virada está em três por três, e abaixo dele a camada custa mais do que economiza. Nosso caso é a primeira linha.

Escrevi a tabela inteira, e não só a linha de cinco por cinco que a literatura costuma citar, justamente porque a primeira linha é a nossa e omiti-la seria vender um argumento que não se aplica. O grupo que copiar o argumento combinatório para justificar a camada num compilador de origem única e destino único está repetindo uma frase, não fazendo engenharia.

A justificativa real, nesta linguagem, é a separação de dificuldades. A tradução da árvore para o objeto tem duas partes independentes: decidir a ordem de avaliação com desvios e decidir a codificação das instruções. Feitas juntas, cada erro de uma parece defeito da outra. A representação intermediária é a fronteira que permite testar a primeira sem a segunda existir — e é exatamente o que este módulo faz, já que a emissão do objeto só vem no módulo seguinte.

Onde é fácil errar aqui. Justificar a camada pelo argumento que não se aplica ao próprio projeto, porque é o argumento que está no livro. A justificativa tem de sair das características da linguagem do grupo, e se ela não existir, a conclusão honesta é que o grupo não precisa da camada — o que também é uma resposta defensável.

Como verificar. A pergunta que uso: se eu apagar a camada e traduzir direto, o que fica mais difícil? Se a resposta for “nada”, a camada é peso morto.

1.3 Tarefa 2: A representação, e por que não a mais simples

A atividade — comparar as formas usuais quanto ao que facilitam e implementar a tradução da árvore verificada.

Comparei três formas sobre a mesma condição, o que é a única comparação que vale — comparar formas sobre exemplos diferentes só mostra que os exemplos são diferentes. O programa de referência tem condições compostas, que é o que separa as formas:

pattern numero = /-?[0-9]+(\.[0-9]+)?/;

rule {
    on numero(n) where value(n) > 100 and value(n) < 500
        => emit("faixa", n);
    on numero(n) where value(n) < 0 or value(n) > 1000
        => emit("extremo", n);
}

A árvore sintática abstrata é a forma que já temos, saída do módulo 10:

  E
    Comparacao ">"
      ValorDe "n"
      LiteralNumero "100"
    Comparacao "<"
      ValorDe "n"
      LiteralNumero "500"

Ela é a mais informativa e a menos executável: a ordem de avaliação está implícita na estrutura, e quem executa precisa reconstruí-la percorrendo.

A notação pós-fixada é a mais curta das três:

  n valor 100 > n valor 500 < and

Ela resolve a ordem de avaliação de graça — a sequência é a ordem — e dispensa temporários e rótulos. E não serve, por uma razão que é o argumento central deste módulo: ela não exprime desvio. Sem desvio não há curto-circuito, e sem curto-circuito o and avalia os dois lados sempre. Numa linguagem em que avaliar não custa nada isso seria só ineficiência; aqui, o módulo seguinte exige curto-circuito explicitamente.

Sobra o código de três endereços, que é o que implementei:

13_ri.cpp
#include "13_ri.h"

#include <sstream>
#include <utility>

namespace peneira {

const char* nomeDaOpRI(OpRI op) noexcept {
    switch (op) {
        case OpRI::Rotulo:        return "rotulo";
        case OpRI::CasamentoDe:   return "casamento";
        case OpRI::Valor:         return "valor";
        case OpRI::Constante:     return "const";
        case OpRI::Comparacao:    return "cmp";
        case OpRI::DesvioSeFalso: return "se_falso";
        case OpRI::Desvio:        return "desvia";
        case OpRI::Emite:         return "emite";
    }
    return "<desconhecida>";
}

std::size_t TradutorRI::emitir(InstrucaoRI instrucao) {
    instrucoes_.push_back(std::move(instrucao));
    return instrucoes_.size() - 1;
}

std::string TradutorRI::novoTemporario() {
    return "t" + std::to_string(proximoTemporario_++);
}

std::size_t TradutorRI::temporariosUsados() const noexcept {
    return proximoTemporario_;
}

void TradutorRI::preencher(const std::vector<std::size_t>& lista,
                           std::size_t destino) {
    for (const std::size_t i : lista) {
        instrucoes_[i].destino = destino;
    }
}

std::string TradutorRI::traduzirValor(const NoAst& expr) {
    switch (expr.tipo) {
        case TipoAst::LiteralNumero: {
            const std::string t = novoTemporario();
            InstrucaoRI ins;
            ins.op = OpRI::Constante;
            ins.resultado = t;
            // O lexema, e não o valor reconvertido: preserva a forma escrita
            // pelo autor no relatório, e a conversão já foi feita e validada no
            // módulo 7.
            ins.arg1 = expr.texto;
            emitir(std::move(ins));
            return t;
        }
        case TipoAst::LiteralTexto: {
            const std::string t = novoTemporario();
            InstrucaoRI ins;
            ins.op = OpRI::Constante;
            ins.resultado = t;
            ins.arg1 = "\"" + expr.texto + "\"";
            emitir(std::move(ins));
            return t;
        }
        case TipoAst::Referencia: {
            const std::string t = novoTemporario();
            InstrucaoRI ins;
            ins.op = OpRI::CasamentoDe;
            ins.resultado = t;
            ins.arg1 = expr.texto;
            emitir(std::move(ins));
            return t;
        }
        case TipoAst::ValorDe: {
            // Duas instruções, e não uma: primeiro o casamento, depois a
            // conversão. Separá-las é o que torna a conversão visível na
            // representação — e a conversão é justamente o que a fase semântica
            // exigiu que fosse explícita no programa fonte.
            const std::string casamento = novoTemporario();
            InstrucaoRI busca;
            busca.op = OpRI::CasamentoDe;
            busca.resultado = casamento;
            busca.arg1 = expr.texto;
            emitir(std::move(busca));

            const std::string t = novoTemporario();
            InstrucaoRI conversao;
            conversao.op = OpRI::Valor;
            conversao.resultado = t;
            conversao.arg1 = casamento;
            emitir(std::move(conversao));
            return t;
        }
        default:
            return "<indefinido>";
    }
}

TradutorRI::ListasDeDesvio TradutorRI::traduzirCondicao(const NoAst& cond) {
    ListasDeDesvio listas;

    switch (cond.tipo) {
        case TipoAst::Comparacao: {
            const std::string esquerda =
                cond.filhos.size() > 0 && cond.filhos[0]
                    ? traduzirValor(*cond.filhos[0])
                    : "<indefinido>";
            const std::string direita =
                cond.filhos.size() > 1 && cond.filhos[1]
                    ? traduzirValor(*cond.filhos[1])
                    : "<indefinido>";

            const std::string t = novoTemporario();
            InstrucaoRI comparacao;
            comparacao.op = OpRI::Comparacao;
            comparacao.resultado = t;
            comparacao.arg1 = esquerda;
            comparacao.arg2 = direita;
            comparacao.operador = cond.texto;
            emitir(std::move(comparacao));

            InstrucaoRI desvio;
            desvio.op = OpRI::DesvioSeFalso;
            desvio.arg1 = t;
            const std::size_t indice = emitir(std::move(desvio));

            // Quando a comparação é falsa, desvia — destino a preencher.
            // Quando é verdadeira, cai para a instrução seguinte, e essa
            // "queda" é o caminho verdadeiro. Não emitir desvio para o caso
            // verdadeiro é o que mantém o código curto.
            listas.falso.push_back(indice);
            return listas;
        }

        case TipoAst::E: {
            // Curto-circuito do E: se o lado esquerdo falha, a expressão
            // inteira falha e o direito NÃO é avaliado. O caminho verdadeiro do
            // esquerdo cai no início do direito; as saídas falsas dos dois se
            // juntam.
            if (cond.filhos.size() < 2 || !cond.filhos[0] || !cond.filhos[1]) {
                return listas;
            }
            ListasDeDesvio esquerda = traduzirCondicao(*cond.filhos[0]);
            // O verdadeiro do esquerdo aponta para onde o direito começa, que é
            // a próxima instrução a ser emitida.
            preencher(esquerda.verdadeiro, instrucoes_.size());
            ListasDeDesvio direita = traduzirCondicao(*cond.filhos[1]);

            listas.verdadeiro = std::move(direita.verdadeiro);
            listas.falso = std::move(esquerda.falso);
            for (const std::size_t i : direita.falso) {
                listas.falso.push_back(i);
            }
            return listas;
        }

        case TipoAst::Ou: {
            // Curto-circuito do OU, simétrico: se o esquerdo é verdadeiro, a
            // expressão inteira é verdadeira. O caminho FALSO do esquerdo cai
            // no direito.
            if (cond.filhos.size() < 2 || !cond.filhos[0] || !cond.filhos[1]) {
                return listas;
            }
            ListasDeDesvio esquerda = traduzirCondicao(*cond.filhos[0]);

            // O verdadeiro do esquerdo tem de pular o lado direito inteiro, e
            // esse destino ainda não existe. Um desvio incondicional carrega a
            // pendência adiante.
            InstrucaoRI salto;
            salto.op = OpRI::Desvio;
            const std::size_t indiceSalto = emitir(std::move(salto));
            preencher(esquerda.verdadeiro, indiceSalto);

            preencher(esquerda.falso, instrucoes_.size());
            ListasDeDesvio direita = traduzirCondicao(*cond.filhos[1]);

            listas.verdadeiro.push_back(indiceSalto);
            for (const std::size_t i : direita.verdadeiro) {
                listas.verdadeiro.push_back(i);
            }
            listas.falso = std::move(direita.falso);
            return listas;
        }

        default:
            return listas;
    }
}

CodigoRI TradutorRI::traduzirAcao(const NoAst& acao) {
    instrucoes_.clear();
    proximoTemporario_ = 0;

    CodigoRI codigo;
    codigo.padrao = acao.texto;
    codigo.ligacao = acao.conteudo;

    ListasDeDesvio listas;
    const bool temCondicao = !acao.filhos.empty() && acao.filhos[0] != nullptr;
    if (temCondicao) {
        listas = traduzirCondicao(*acao.filhos[0]);
        // O caminho verdadeiro segue para a emissão, que é a próxima
        // instrução.
        preencher(listas.verdadeiro, instrucoes_.size());
    }

    // A emissão.
    if (!acao.filhos.empty() && acao.filhos.back() &&
        acao.filhos.back()->tipo == TipoAst::Emissao) {
        const NoAst& emissao = *acao.filhos.back();
        std::string valor = "<indefinido>";
        if (!emissao.filhos.empty() && emissao.filhos[0]) {
            valor = traduzirValor(*emissao.filhos[0]);
        }
        InstrucaoRI ins;
        ins.op = OpRI::Emite;
        ins.arg1 = "\"" + emissao.texto + "\"";
        ins.arg2 = valor;
        emitir(std::move(ins));
    }

    // Fim da regra. Todo desvio de condição falsa termina aqui, e este é o
    // momento em que o endereço finalmente se conhece: é o índice logo após a
    // última instrução.
    preencher(listas.falso, instrucoes_.size());

    codigo.instrucoes = std::move(instrucoes_);
    return codigo;
}

std::string formatarRI(const CodigoRI& c) {
    std::ostringstream out;
    out << "  regra: on " << c.padrao << "(" << c.ligacao << ")\n";
    for (std::size_t i = 0; i < c.instrucoes.size(); ++i) {
        const InstrucaoRI& ins = c.instrucoes[i];
        out << "    " << i << ": ";
        switch (ins.op) {
            case OpRI::Constante:
                out << ins.resultado << " := " << ins.arg1;
                break;
            case OpRI::CasamentoDe:
                out << ins.resultado << " := casamento " << ins.arg1;
                break;
            case OpRI::Valor:
                out << ins.resultado << " := valor " << ins.arg1;
                break;
            case OpRI::Comparacao:
                out << ins.resultado << " := " << ins.arg1 << " "
                    << ins.operador << " " << ins.arg2;
                break;
            case OpRI::DesvioSeFalso:
                out << "se_falso " << ins.arg1 << " desvia para "
                    << ins.destino;
                break;
            case OpRI::Desvio:
                out << "desvia para " << ins.destino;
                break;
            case OpRI::Emite:
                out << "emite " << ins.arg1 << ", " << ins.arg2;
                break;
            case OpRI::Rotulo:
                out << "rotulo " << ins.resultado;
                break;
        }
        out << '\n';
    }
    out << "    " << c.instrucoes.size() << ": (fim da regra)\n";
    return out.str();
}

std::string emPosFixada(const NoAst& expr) {
    switch (expr.tipo) {
        case TipoAst::LiteralNumero:
        case TipoAst::LiteralTexto:
            return expr.texto;
        case TipoAst::Referencia:
            return expr.texto;
        case TipoAst::ValorDe:
            return expr.texto + " valor";
        case TipoAst::Comparacao: {
            const std::string e = expr.filhos.size() > 0 && expr.filhos[0]
                                      ? emPosFixada(*expr.filhos[0])
                                      : "";
            const std::string d = expr.filhos.size() > 1 && expr.filhos[1]
                                      ? emPosFixada(*expr.filhos[1])
                                      : "";
            return e + " " + d + " " + expr.texto;
        }
        case TipoAst::E:
        case TipoAst::Ou: {
            const std::string e = expr.filhos.size() > 0 && expr.filhos[0]
                                      ? emPosFixada(*expr.filhos[0])
                                      : "";
            const std::string d = expr.filhos.size() > 1 && expr.filhos[1]
                                      ? emPosFixada(*expr.filhos[1])
                                      : "";
            return e + " " + d + (expr.tipo == TipoAst::E ? " and" : " or");
        }
        default:
            return "";
    }
}

CustoDeTraducao custoDeTraducao(std::size_t origens,
                                std::size_t destinos) noexcept {
    CustoDeTraducao c;
    c.origens = origens;
    c.destinos = destinos;
    c.tradutoresDiretos = origens * destinos;
    c.tradutoresComIntermediaria = origens + destinos;
    return c;
}

}  // namespace peneira

Uma decisão de tradução merece destaque porque não é óbvia. A construção value(n) vira duas instruções, e não uma: primeiro o casamento é buscado, depois convertido.

    0: t0 := casamento n
    1: t1 := valor t0

Poderia ser uma instrução só, e seria mais curto. Separei porque a conversão é justamente o que a análise semântica do módulo 12 exigiu que fosse explícita no programa fonte — foi a razão de recusar conversão implícita. Colapsá-la aqui esconderia na representação intermediária a distinção que a fase anterior lutou para tornar visível.

Sobre a forma de atribuição única estática, que a ementa menciona: ela não foi implementada, e o motivo é que ela existe para viabilizar otimizações sobre fluxo de dados que este compilador não faz. Registro a ausência em vez de fingir cobertura.

Registro também um limite do artefato, porque ele afeta o que este módulo consegue demonstrar. A tradução de condicionais está exercitada — a condição where é um condicional, e é dela que saem os desvios acima. A de laços não está, porque a Peneira não tem laço nenhum no nível do usuário: a repetição da linguagem é o laço de varredura do executor, que é parte do modelo de execução e não uma construção que o programador escreva. O mecanismo é o mesmo e a diferença é uma só: um laço precisa de desvio para trás, para uma instrução já emitida, e por isso o seu destino é conhecido no momento em que o desvio é gerado — é o caso fácil, não o difícil. Tudo que aparece aqui é desvio para a frente, que é justamente o que exige o preenchimento retroativo. Um grupo cuja linguagem tenha laços encontra as duas situações; o nosso só encontra a pior delas.

Onde é fácil errar aqui. Escolher a representação pela elegância e descobrir depois que ela não exprime o que a linguagem precisa. A pós-fixada é claramente a mais bonita das três aqui, e é a errada. O teste tem de ser feito contra a construção mais difícil da linguagem — no nosso caso, a condição composta —, não contra a mais fácil.

Como verificar. Cada instrução gerada tem de ter destino definido e cada temporário tem de ser escrito antes de lido. A demonstração --formasri imprime as três formas lado a lado, e a contagem de temporários fecha com o número de subexpressões da condição.

1.4 Tarefa 3: O formato do objeto, e a divergência que precisei resolver

A atividade — especificar o formato do programa objeto, completo o bastante para permitir implementação independente.

Este é o entregável principal, e escrevi a especificação antes do código, que é a ordem que a atividade exige e a que os grupos costumam inverter. O arquivo de cabeçalho é a especificação — os campos vêm com semântica declarada, não apenas com nome:

13_objeto.h
#ifndef PENEIRA_13_OBJETO_H
#define PENEIRA_13_OBJETO_H

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

#include "03_afd.h"

namespace peneira {

// O FORMATO DO PROGRAMA OBJETO DA PENEIRA.
//
// Este cabeçalho é a especificação. O critério de qualidade anunciado para o
// módulo é literal: outra pessoa tem de conseguir escrever um executor
// compatível lendo apenas isto. Por isso os campos vêm com semântica declarada,
// e não apenas com nome.
//
// O objeto tem duas seções, conforme o design do artefato:
//
//   1. A TABELA DE PADRÕES — um autômato finito determinístico por `pattern`,
//      serializado como tabela de transição.
//   2. O CÓDIGO DAS REGRAS — para cada ação, uma sequência de instruções de uma
//      máquina de pilha.
//
// A escolha de máquina de pilha, e não de registradores, tem motivo: as
// expressões da Peneira são rasas (uma comparação, no máximo alguns conectivos)
// e a máquina de pilha dispensa alocação de registradores por completo. Numa
// linguagem com expressões profundas a conta se inverteria.

// ---------------------------------------------------------------------------
// Conjunto de instruções
// ---------------------------------------------------------------------------
//
// As sete primeiras vêm do design do artefato. As duas de desvio são
// ACRÉSCIMO DELIBERADO, e registro o porquê em vez de silenciá-lo: o design
// lista `AND` e `OR` como instruções, o que implica avaliação ansiosa dos dois
// lados; o curto-circuito exigido na geração de código do módulo seguinte não
// é exprimível sem desvio. Mantive `AND` e `OR` no conjunto — elas continuam
// válidas e são o que um gerador ansiado emitiria — e acrescentei os desvios,
// que são o que o nosso gerador vai usar. Um executor compatível precisa
// implementar as nove.
enum class OpCode : std::uint8_t {
    PUSH_CONST = 0,   // empilha a constante de índice `argumento`
    PUSH_MATCH = 1,   // empilha o texto casado pela ligação de índice `argumento`
    VALUE = 2,        // desempilha texto, empilha o número correspondente
    CMP_GT = 3,       // desempilha b, a; empilha (a > b)
    CMP_LT = 4,
    CMP_GE = 5,
    CMP_LE = 6,
    CMP_EQ = 7,
    CMP_NE = 8,
    AND = 9,          // desempilha b, a; empilha (a e b) — avaliação ansiosa
    OR = 10,          // desempilha b, a; empilha (a ou b) — avaliação ansiosa
    JUMP_IF_FALSE = 11,  // desempilha; se falso, desvia para `argumento`
    JUMP = 12,           // desvia para `argumento`
    EMIT = 13,        // desempilha valor; emite com o rótulo de índice `argumento`
    HALT = 14,        // encerra a execução desta regra
};

const char* nomeDoOpCode(OpCode op) noexcept;

// Toda instrução ocupa dois campos: o código e um argumento inteiro. As que não
// usam argumento gravam zero. O tamanho fixo é decisão consciente — dispensa
// decodificação de comprimento variável no executor, ao custo de alguns bytes
// que não importam nesta escala.
struct Instrucao {
    OpCode op = OpCode::HALT;
    std::uint32_t argumento = 0;
};

// ---------------------------------------------------------------------------
// Seções do objeto
// ---------------------------------------------------------------------------

// Um padrão compilado, serializado como tabela de transição.
//
// `transicoes` é a matriz achatada: a transição do estado `e` pelo símbolo `s`
// está em `transicoes[e * 256 + s]`. O valor kSemTransicao marca ausência, que
// o executor trata como falha de casamento.
//
// A matriz densa é escolha de FORMATO, e diverge da representação esparsa que o
// compilador usa internamente desde o módulo 3. O motivo é que os papéis são
// diferentes: dentro do compilador, o autômato é construído e transformado, e a
// esparsa economiza memória num alfabeto de 256 símbolos; no objeto, ele só é
// consultado, e a densa dá consulta em tempo constante sem busca. Trocar de
// representação na fronteira entre as duas fases é a decisão certa, e é o tipo
// de coisa que só fica evidente quando a especificação é escrita antes do
// código.
inline constexpr std::uint32_t kSemTransicao = 0xFFFFFFFFu;
inline constexpr std::size_t kTamanhoDoAlfabeto = 256;

struct PadraoObjeto {
    std::string nome;
    std::uint32_t quantidadeDeEstados = 0;
    std::uint32_t estadoInicial = 0;
    std::vector<std::uint8_t> finais;      // 1 por estado
    std::vector<std::uint32_t> transicoes; // achatada, 256 por estado
    bool casaSomenteNumeros = false;       // do verificador de tipos
};

// O código de uma regra.
struct RegraObjeto {
    std::uint32_t indiceDoPadrao = 0;  // qual padrão dispara esta regra
    std::string ligacao;               // nome da variável, para diagnóstico
    std::vector<Instrucao> codigo;
};

// O programa objeto completo.
struct ProgramaObjeto {
    // Constantes agrupadas numa área única, referenciadas por índice. Textos e
    // números moram no mesmo vetor, na forma textual: a distinção de tipo já
    // foi resolvida na análise semântica, e o executor sabe qual instrução lê o
    // quê.
    std::vector<std::string> constantes;
    std::vector<PadraoObjeto> padroes;
    std::vector<RegraObjeto> regras;

    // Registra a constante e devolve o índice, reaproveitando se já existir.
    std::uint32_t adicionarConstante(const std::string& valor);
};

// Converte o autômato interno para a forma do objeto.
PadraoObjeto serializarPadrao(const Afd& afd, const std::string& nome,
                              bool casaSomenteNumeros);

// Forma textual do objeto, legível e suficiente para conferência manual.
std::string formatarObjeto(const ProgramaObjeto& p);

// O MODELO DE EXECUÇÃO, em texto, para acompanhar a especificação dos campos.
// Está em código, e não só no material, porque a especificação e o
// implementador precisam ler a mesma coisa.
std::string modeloDeExecucao();

}  // namespace peneira

#endif  // PENEIRA_13_OBJETO_H

O objeto tem duas seções, conforme o design do artefato: uma tabela com um autômato determinístico por padrão, e o código das regras para uma máquina de pilha. A escolha de máquina de pilha em vez de registradores se justifica porque as expressões da Peneira são rasas — uma comparação, no máximo alguns conectivos — e a pilha dispensa alocação de registradores por completo. Numa linguagem com expressões profundas a conta se inverteria.

1.4.1 A divergência, e como a resolvi

Ao escrever o conjunto de instruções encontrei um conflito entre dois documentos que eu mesmo tinha produzido, e prefiro registrá-lo a silenciá-lo.

O design do artefato lista sete instruções, entre elas AND e OR. Ter AND e OR como instruções significa avaliação ansiosa: os dois operandos são calculados e empilhados, e a instrução combina. Só que a especificação do projeto integrador exige, para o módulo seguinte, que a avaliação com curto-circuito seja tratada — e curto-circuito não é exprimível sem desvio.

Resolvi acrescentando duas instruções de desvio ao conjunto e mantendo AND e OR, e registrando a decisão no próprio cabeçalho. As sete originais continuam válidas e são o que um gerador ansioso emitiria; os desvios são o que o nosso gerador vai usar. Acrescentei também as demais comparações, já que o design nomeia CMP_GT como representante de uma família e não como uma instrução isolada. O conjunto fica, portanto, com quinze códigos de operação: as sete do design original, as duas de desvio que acabo de justificar, e as seis da família de comparação — e é essa a conta que um executor compatível precisa implementar. Quando o texto fala em nove instruções, refere-se ao núcleo do design com CMP_GT valendo pela família inteira; o cabeçalho expande a família, e é ele que vale.

O que não fiz foi mudar o design em silêncio nem torcer o conjunto de instruções para caber na lista original. Divergência entre documentos de projeto acontece; o que não pode acontecer é ela ser resolvida sem ficar escrita.

1.4.2 A decisão de formato que a especificação revelou

Escrever a especificação antes fez aparecer uma decisão que eu teria tomado errado por inércia. Dentro do compilador, a função de transição é um mapa esparso por estado — decisão do módulo 3, tomada porque o alfabeto tem 256 símbolos e a maior parte não é usada. No objeto, gravei uma matriz densa.

A troca é deliberada, porque os papéis são diferentes: dentro do compilador o autômato é construído e transformado, e a esparsa economiza; no objeto ele só é consultado, e a densa dá consulta em tempo constante sem busca.

E aqui a medição desmente a intuição confortável. A demonstração --objeto imprime a densidade real:

    [0] numero: 5 estados, inicial 0, 2 final(is)
        celulas: 1280, preenchidas: 52

Quatro por cento. A matriz densa desperdiça noventa e seis por cento do espaço que ocupa, e não adianta esconder isso. A defesa da escolha tem de ser feita com o número na mesa: são 5120 bytes por padrão, o laço interno do executor consulta essa tabela uma vez por byte da entrada, e nessa posição a consulta em tempo constante vale mais que os kilobytes. A defesa tem prazo de validade — se o número de padrões crescer a ponto de as tabelas não caberem em cache, a decisão precisa ser revista, e é esse número impresso que vai permitir notar.

1.4.3 O modelo de execução

A especificação dos campos não basta: quem for escrever o executor precisa saber o que a máquina faz com eles. O modelo está no código, e não só neste texto, porque a especificação e o implementador precisam ler a mesma coisa:

13_objeto.cpp
#include "13_objeto.h"

#include <sstream>

namespace peneira {

const char* nomeDoOpCode(OpCode op) noexcept {
    switch (op) {
        case OpCode::PUSH_CONST:    return "PUSH_CONST";
        case OpCode::PUSH_MATCH:    return "PUSH_MATCH";
        case OpCode::VALUE:         return "VALUE";
        case OpCode::CMP_GT:        return "CMP_GT";
        case OpCode::CMP_LT:        return "CMP_LT";
        case OpCode::CMP_GE:        return "CMP_GE";
        case OpCode::CMP_LE:        return "CMP_LE";
        case OpCode::CMP_EQ:        return "CMP_EQ";
        case OpCode::CMP_NE:        return "CMP_NE";
        case OpCode::AND:           return "AND";
        case OpCode::OR:            return "OR";
        case OpCode::JUMP_IF_FALSE: return "JUMP_IF_FALSE";
        case OpCode::JUMP:          return "JUMP";
        case OpCode::EMIT:          return "EMIT";
        case OpCode::HALT:          return "HALT";
    }
    return "<desconhecido>";
}

std::uint32_t ProgramaObjeto::adicionarConstante(const std::string& valor) {
    for (std::size_t i = 0; i < constantes.size(); ++i) {
        if (constantes[i] == valor) {
            return static_cast<std::uint32_t>(i);
        }
    }
    constantes.push_back(valor);
    return static_cast<std::uint32_t>(constantes.size() - 1);
}

PadraoObjeto serializarPadrao(const Afd& afd, const std::string& nome,
                              bool casaSomenteNumeros) {
    PadraoObjeto p;
    p.nome = nome;
    p.quantidadeDeEstados = static_cast<std::uint32_t>(afd.quantidadeDeEstados());
    p.estadoInicial = static_cast<std::uint32_t>(afd.inicial());
    p.casaSomenteNumeros = casaSomenteNumeros;

    p.finais.resize(afd.quantidadeDeEstados(), 0);
    p.transicoes.assign(afd.quantidadeDeEstados() * kTamanhoDoAlfabeto,
                        kSemTransicao);

    for (std::size_t e = 0; e < afd.quantidadeDeEstados(); ++e) {
        p.finais[e] = afd.ehFinal(e) ? std::uint8_t{1} : std::uint8_t{0};
        for (std::size_t s = 0; s < kTamanhoDoAlfabeto; ++s) {
            const Estado destino =
                afd.transicao(e, static_cast<Simbolo>(s));
            if (destino != kSemEstado) {
                p.transicoes[e * kTamanhoDoAlfabeto + s] =
                    static_cast<std::uint32_t>(destino);
            }
        }
    }
    return p;
}

std::string formatarObjeto(const ProgramaObjeto& p) {
    std::ostringstream out;

    out << "  area de constantes (" << p.constantes.size() << "):\n";
    for (std::size_t i = 0; i < p.constantes.size(); ++i) {
        out << "    [" << i << "] " << p.constantes[i] << '\n';
    }

    out << "\n  tabela de padroes (" << p.padroes.size() << "):\n";
    for (std::size_t i = 0; i < p.padroes.size(); ++i) {
        const PadraoObjeto& pd = p.padroes[i];
        // Conta as transições preenchidas para mostrar a densidade real da
        // matriz — o número que justifica ou condena a escolha de formato.
        std::size_t preenchidas = 0;
        for (const std::uint32_t t : pd.transicoes) {
            if (t != kSemTransicao) {
                ++preenchidas;
            }
        }
        std::size_t finais = 0;
        for (const std::uint8_t f : pd.finais) {
            finais += f;
        }
        out << "    [" << i << "] " << pd.nome << ": "
            << pd.quantidadeDeEstados << " estados, inicial "
            << pd.estadoInicial << ", " << finais << " final(is)\n"
            << "        celulas: " << pd.transicoes.size() << ", preenchidas: "
            << preenchidas << '\n'
            << "        casa somente numeros: "
            << (pd.casaSomenteNumeros ? "sim" : "nao") << '\n';
    }

    out << "\n  codigo das regras (" << p.regras.size() << "):\n";
    for (std::size_t i = 0; i < p.regras.size(); ++i) {
        const RegraObjeto& r = p.regras[i];
        out << "    regra [" << i << "] padrao ["
            << r.indiceDoPadrao << "] ligacao '" << r.ligacao << "'\n";
        for (std::size_t k = 0; k < r.codigo.size(); ++k) {
            const Instrucao& ins = r.codigo[k];
            out << "      " << k << ": " << nomeDoOpCode(ins.op);
            switch (ins.op) {
                case OpCode::PUSH_CONST:
                case OpCode::EMIT:
                    out << " " << ins.argumento << "  ; "
                        << (ins.argumento < p.constantes.size()
                                ? p.constantes[ins.argumento]
                                : std::string("<fora de faixa>"));
                    break;
                case OpCode::PUSH_MATCH:
                case OpCode::JUMP:
                case OpCode::JUMP_IF_FALSE:
                    out << " " << ins.argumento;
                    break;
                default:
                    break;
            }
            out << '\n';
        }
    }
    return out.str();
}

std::string modeloDeExecucao() {
    // Escrito como contrato para quem for implementar o executor, e não como
    // descrição do que o nosso executor faz — ainda não existe executor. A
    // diferença importa: um descreve o que se pode assumir, o outro descreve o
    // que aconteceu de ser implementado.
    return
        "  ESTADO DA MAQUINA\n"
        "    - a entrada, lida como sequencia de bytes\n"
        "    - uma posicao de leitura, que so avanca\n"
        "    - uma pilha de valores, vazia no inicio de cada regra\n"
        "    - o ambiente de casamento: o texto casado e o padrao que casou\n\n"
        "  LACO PRINCIPAL\n"
        "    Na posicao corrente, o executor tenta TODOS os padroes em\n"
        "    paralelo, avancando enquanto algum automato ainda tem transicao.\n"
        "    Registra a ultima posicao em que algum automato esteve em estado\n"
        "    final, e qual padrao. Ao travar, retrocede ate essa posicao: e a\n"
        "    regra do CASAMENTO MAIS LONGO, a mesma do analisador lexico.\n"
        "    Empate entre padroes no mesmo comprimento resolve-se pela ORDEM DE\n"
        "    DECLARACAO no programa fonte — o primeiro declarado vence.\n"
        "    Sem casamento nenhum, a posicao avanca um byte e o laco recomeca.\n\n"
        "  DISPARO DA REGRA\n"
        "    Com um casamento, o executor liga o texto casado a variavel da\n"
        "    regra correspondente e executa o codigo daquela regra desde a\n"
        "    instrucao 0, com a pilha vazia. A execucao termina em HALT ou ao\n"
        "    passar da ultima instrucao.\n"
        "    Terminada a regra, a posicao de leitura avanca para o fim do\n"
        "    casamento e o laco principal recomeca.\n\n"
        "  SEMANTICA DAS INSTRUCOES\n"
        "    PUSH_CONST k   empilha constantes[k]\n"
        "    PUSH_MATCH k   empilha o texto casado (k identifica a ligacao)\n"
        "    VALUE          desempilha texto, empilha o numero que ele denota\n"
        "    CMP_*          desempilha b, depois a; empilha o resultado de a<op>b\n"
        "    AND / OR       desempilha b, depois a; empilha a combinacao\n"
        "    JUMP_IF_FALSE k  desempilha; se falso, a proxima instrucao e k\n"
        "    JUMP k         a proxima instrucao e k\n"
        "    EMIT k         desempilha o valor e emite o par (constantes[k], valor)\n"
        "    HALT           encerra esta regra\n\n"
        "  CONDICOES DE ERRO QUE O EXECUTOR PRECISA TRATAR\n"
        "    - VALUE sobre texto que nao denota numero: nao pode ocorrer, porque\n"
        "      a analise semantica provou por inclusao de linguagens que o\n"
        "      padrao so casa numeros. Um executor defensivo aborta com\n"
        "      diagnostico em vez de produzir valor arbitrario.\n"
        "    - desvio para fora da faixa de instrucoes: objeto malformado.\n"
        "    - pilha vazia numa instrucao que desempilha: objeto malformado.\n";
}

}  // namespace peneira

Três pontos do modelo mereceram decisão explícita, e são justamente os que faltariam numa especificação apressada. O casamento mais longo é a mesma regra do analisador léxico do módulo 7, e reusá-la é o que mantém a linguagem coerente consigo mesma. O empate entre padrões de mesmo comprimento resolve-se pela ordem de declaração no fonte — precisa estar escrito, porque sem isso dois executores corretos produziriam saídas diferentes para o mesmo programa. E as condições de erro estão listadas, inclusive uma que não pode ocorrer: VALUE sobre texto não numérico é impossível porque a análise semântica provou por inclusão de linguagens que o padrão só casa números — e mesmo assim o executor defensivo deve abortar com diagnóstico, em vez de produzir valor arbitrário se o objeto tiver sido adulterado.

A seção de regras sai vazia nesta demonstração, e de propósito: preenchê-la é geração de código, que é o módulo seguinte. Este módulo entrega o formato e a representação intermediária; o próximo traduz uma na outra.

Onde é fácil errar aqui. Especificar depois de implementar. A especificação escrita a partir do código já existente documenta as decisões acidentais junto com as deliberadas, e não tem como distinguir umas das outras — quem lê não sabe o que pode assumir. As duas decisões acima, a matriz densa e o desempate por ordem de declaração, só apareceram como decisões porque a especificação veio antes.

Como verificar. O critério literal: entregar apenas a especificação a alguém e pedir que descreva o que o executor faz com uma entrada dada. Onde a pessoa precisar perguntar, falta texto. Foi assim que descobri que o desempate entre padrões não estava especificado.

1.5 Tarefa 4: O ambiente de execução

A atividade — descrever a organização da memória em execução e o que o compilador precisa emitir para sustentá-la.

13_ambiente.h
#ifndef PENEIRA_13_AMBIENTE_H
#define PENEIRA_13_AMBIENTE_H

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

namespace peneira {

// AMBIENTES DE EXECUÇÃO.
//
// Duas coisas moram neste arquivo, e é preciso separá-las com clareza porque
// só a primeira é o nosso artefato:
//
//   1. O ambiente de execução DA PENEIRA — a divisão de memória que o programa
//      objeto exige e o registro de ativação de uma regra. Isso é o sistema.
//
//   2. Um EXEMPLO MÍNIMO ISOLADO de registro de ativação com procedimentos
//      aninhados, cadeia de controle e cadeia de acesso. Isso NÃO faz parte do
//      compilador da Peneira, e não faria sentido embutir: a linguagem não tem
//      procedimentos definidos pelo usuário, e inventar uns só para exercitar
//      o mecanismo distorceria o artefato. Fica aqui como exemplo separado,
//      pelo mesmo padrão do exemplo de tradução do módulo 1.
//
// O motivo de não omitir o segundo é que ele é o repertório necessário para ler
// qualquer linguagem real, e a conexão com a organização de memória vista em
// arquitetura de computadores é direta.

// ---------------------------------------------------------------------------
// 1. O ambiente de execução da Peneira
// ---------------------------------------------------------------------------

// As quatro áreas em que a memória de um programa em execução se divide, com o
// que cada uma guarda no caso da Peneira. Tamanho zero significa "cresce em
// execução".
enum class AreaDeMemoria : std::uint8_t {
    Codigo,     // o código das regras; imutável, tamanho conhecido na compilação
    Estatica,   // constantes e tabelas de transição; tamanho conhecido também
    Pilha,      // a pilha de avaliação da regra corrente
    Monte,      // o texto casado, cujo tamanho só se conhece em execução
};

const char* nomeDaArea(AreaDeMemoria a) noexcept;

struct DescricaoDeArea {
    AreaDeMemoria area;
    std::string conteudo;
    bool tamanhoConhecidoNaCompilacao = false;
    std::size_t bytesNesteObjeto = 0;  // 0 quando cresce em execução
};

// Descreve o mapa de memória do objeto dado, com os tamanhos reais das áreas
// estáticas. É o que permite dizer, com número, quanto do programa é decidido
// na compilação e quanto sobra para a execução.
struct ProgramaObjeto;  // declarado em 13_objeto.h
std::vector<DescricaoDeArea> mapaDeMemoria(const ProgramaObjeto& objeto);

// O registro de ativação de uma regra da Peneira.
//
// A Peneira não tem chamada de procedimento, e mesmo assim tem registro de
// ativação — porque disparar uma regra É uma ativação: há um ponto de entrada,
// um conjunto de valores ligados na entrada, um espaço de trabalho próprio e um
// retorno. O registro é pequeno porque a linguagem é pequena, e mostrar que ele
// existe é o que liga o conceito ao artefato em vez de deixá-lo abstrato.
struct AtivacaoDeRegra {
    std::uint32_t regra = 0;
    std::size_t posicaoNaEntrada = 0;  // onde o casamento começou
    std::string textoCasado;           // o parâmetro, em essência
    std::size_t topoDaPilha = 0;       // espaço de trabalho
    std::size_t enderecoDeRetorno = 0; // para onde o laço principal volta
};

std::string descreverAtivacao(const AtivacaoDeRegra& a);

// Por que NÃO há cadeia de controle nem de acesso na Peneira: as ativações não
// se aninham. Uma regra dispara, executa e termina antes de a próxima começar,
// então a "pilha" de ativações tem no máximo um elemento. É o que torna o
// registro acima um registro só, e não uma pilha deles.
std::size_t profundidadeMaximaDeAtivacao() noexcept;

// ---------------------------------------------------------------------------
// 2. Exemplo mínimo isolado: aninhamento, cadeia de controle e de acesso
// ---------------------------------------------------------------------------
//
// NÃO faz parte do compilador da Peneira. Existe para exercitar o mecanismo que
// a linguagem não tem, sobre um programa hipotético com procedimentos
// aninhados.

// Um procedimento do exemplo: nome, nível de aninhamento léxico e quem o
// contém.
struct ProcedimentoExemplo {
    std::string nome;
    std::size_t nivelLexico = 0;
    std::size_t contidoEm = 0;  // índice do procedimento envolvente
};

// Um quadro na pilha de execução do exemplo.
struct Quadro {
    std::size_t procedimento = 0;
    // Cadeia de CONTROLE: quem me chamou. Segue a ordem de EXECUÇÃO.
    std::size_t eloDeControle = 0;
    // Cadeia de ACESSO: o quadro do procedimento que me CONTÉM no texto do
    // programa. Segue a ordem LÉXICA. As duas coincidem em muitos casos e
    // divergem exatamente quando a recursão ou a chamada cruzada entram —
    // e é essa divergência que justifica manter as duas.
    std::size_t eloDeAcesso = 0;
    bool temElo = false;
};

// Os procedimentos do exemplo: um programa principal contendo `externo`, que
// contém `interno`.
const std::vector<ProcedimentoExemplo>& procedimentosDoExemplo();

// Simula a sequência de chamadas dada (por índice de procedimento) e devolve a
// pilha de quadros resultante, com as duas cadeias montadas.
std::vector<Quadro> simularChamadas(const std::vector<std::size_t>& chamadas);

std::string desenharPilha(const std::vector<Quadro>& pilha);

// Resolve o acesso a uma variável declarada no nível léxico dado, a partir do
// topo da pilha, seguindo a cadeia de ACESSO. Devolve o índice do quadro que a
// contém, ou o tamanho da pilha se não achar.
//
// É este percurso que torna a cadeia de acesso necessária: seguir a de controle
// aqui daria a variável do chamador, que pode não ser o mesmo procedimento que
// envolve este no texto.
std::size_t resolverAcesso(const std::vector<Quadro>& pilha,
                           std::size_t nivelLexicoAlvo);

}  // namespace peneira

#endif  // PENEIRA_13_AMBIENTE_H

A memória de um programa Peneira em execução divide-se em quatro áreas, e a divisão útil não é a nominal — é a que separa o que o compilador sabe do que só a execução descobre:

  area     | tamanho conhecido na compilacao | bytes
  ---------+--------------------------------+-------
  codigo   |              sim               | 0
  estatica |              sim               | 5137
  pilha    |              nao               | cresce
  monte    |              nao               | cresce

O código das regras e a área estática — tabelas de transição e constantes — têm tamanho fixado na compilação. A pilha de avaliação cresce em execução, mas com um detalhe que vale registrar: a profundidade máxima dela é conhecida na compilação, porque é a da expressão mais aninhada do programa. É informação que o compilador poderia emitir no objeto para o executor pré-alocar, e anoto como possibilidade não implementada em vez de deixar implícito. Só o texto casado é genuinamente dinâmico, porque o comprimento do casamento só se conhece ao casar.

A área de código aparece com zero bytes porque o módulo 14 ainda não emitiu nada. Essa linha da tabela custou uma correção: na primeira versão, tamanho zero e tamanho dinâmico apareciam iguais, e a área de código — que é a mais estática de todas — parecia crescer em execução. Distinguir “zero” de “cresce” é o tipo de detalhe que só aparece rodando.

1.5.1 O registro de ativação de uma regra

A Peneira não tem chamada de procedimento e mesmo assim tem registro de ativação, porque disparar uma regra é uma ativação: há entrada, valores ligados na entrada, espaço de trabalho próprio e retorno.

    registro de ativacao da regra 0:
      posicao na entrada ... 42
      texto casado ......... "250" (3 bytes)
      topo da pilha ........ 0
      endereco de retorno .. 45

O texto casado é, em essência, o parâmetro; o endereço de retorno é para onde o laço principal volta. Mostrar que o registro existe neste artefato é o que liga o conceito ao projeto, em vez de deixá-lo abstrato.

E há um resultado que decide o resto: a profundidade máxima da pilha de ativações é um. Uma regra termina antes de a próxima começar, então as ativações não se aninham. É por isso que a Peneira não precisa de cadeia de controle nem de cadeia de acesso — e é por isso que esse mecanismo, que a ementa pede, não cabe no sistema principal.

Onde é fácil errar aqui. Descrever a divisão de memória copiando a divisão clássica de uma linguagem com procedimentos, sem verificar quais áreas a linguagem do grupo realmente usa. Uma linguagem sem alocação dinâmica não tem monte, e listá-lo mesmo assim é preencher formulário.

Como verificar. Some os bytes das áreas estáticas e compare com o tamanho do objeto gravado. Se não bater, alguma coisa está sendo emitida sem estar no mapa.

1.6 Tarefa 5: O mecanismo que a linguagem dispensa

A atividade — explicar o protocolo de chamada e retorno, o escopo em tempo de execução e as cadeias de acesso e de controle.

A ementa pede este conteúdo e a Peneira não o exercita. Tinha duas saídas ruins e escolhi uma terceira.

A primeira saída ruim seria omitir, alegando que o artefato não usa. É o repertório necessário para ler qualquer linguagem real, e a conexão com a organização de memória vista em arquitetura de computadores é direta demais para ser desperdiçada.

A segunda seria inventar procedimentos na Peneira só para exercitar o mecanismo. Distorceria o artefato: a linguagem não precisa deles, e acrescentá-los por razão didática produziria uma característica que nenhuma outra parte do sistema usa — exatamente o que os módulos 9 e 11 recusaram fazer com o autômato de pilha e o gerador ascendente.

A terceira, que é a que tomei, é manter um exemplo mínimo isolado dentro do projeto, claramente separado do sistema principal — o mesmo padrão do exemplo de interpretação contra compilação do módulo 1:

13_ambiente.cpp
#include "13_ambiente.h"

#include <sstream>

#include "13_objeto.h"

namespace peneira {

const char* nomeDaArea(AreaDeMemoria a) noexcept {
    switch (a) {
        case AreaDeMemoria::Codigo:   return "codigo";
        case AreaDeMemoria::Estatica: return "estatica";
        case AreaDeMemoria::Pilha:    return "pilha";
        case AreaDeMemoria::Monte:    return "monte";
    }
    return "<desconhecida>";
}

std::vector<DescricaoDeArea> mapaDeMemoria(const ProgramaObjeto& objeto) {
    std::vector<DescricaoDeArea> areas;

    std::size_t bytesDeCodigo = 0;
    for (const RegraObjeto& r : objeto.regras) {
        bytesDeCodigo += r.codigo.size() * sizeof(Instrucao);
    }

    std::size_t bytesEstaticos = 0;
    for (const PadraoObjeto& p : objeto.padroes) {
        bytesEstaticos += p.transicoes.size() * sizeof(std::uint32_t);
        bytesEstaticos += p.finais.size() * sizeof(std::uint8_t);
    }
    for (const std::string& c : objeto.constantes) {
        bytesEstaticos += c.size();
    }

    areas.push_back(DescricaoDeArea{
        AreaDeMemoria::Codigo,
        "o codigo das regras; imutavel e nunca realocado", true,
        bytesDeCodigo});
    areas.push_back(DescricaoDeArea{
        AreaDeMemoria::Estatica,
        "tabelas de transicao dos padroes e area de constantes", true,
        bytesEstaticos});
    areas.push_back(DescricaoDeArea{
        AreaDeMemoria::Pilha,
        "pilha de avaliacao da regra corrente; profundidade limitada pela "
        "expressao mais aninhada, conhecida na compilacao",
        false, 0});
    areas.push_back(DescricaoDeArea{
        AreaDeMemoria::Monte,
        "o texto casado, cujo comprimento so se conhece ao casar", false, 0});

    return areas;
}

std::string descreverAtivacao(const AtivacaoDeRegra& a) {
    std::ostringstream out;
    out << "    registro de ativacao da regra " << a.regra << ":\n"
        << "      posicao na entrada ... " << a.posicaoNaEntrada << '\n'
        << "      texto casado ......... \"" << a.textoCasado << "\" ("
        << a.textoCasado.size() << " bytes)\n"
        << "      topo da pilha ........ " << a.topoDaPilha << '\n'
        << "      endereco de retorno .. " << a.enderecoDeRetorno << '\n';
    return out.str();
}

std::size_t profundidadeMaximaDeAtivacao() noexcept { return 1; }

// ---------------------------------------------------------------------------
// Exemplo mínimo isolado
// ---------------------------------------------------------------------------

const std::vector<ProcedimentoExemplo>& procedimentosDoExemplo() {
    // principal contem externo, que contem interno. Tres niveis, que e o
    // minimo para a cadeia de acesso divergir da de controle.
    static const std::vector<ProcedimentoExemplo> procs = {
        ProcedimentoExemplo{"principal", 0, 0},
        ProcedimentoExemplo{"externo", 1, 0},
        ProcedimentoExemplo{"interno", 2, 1},
    };
    return procs;
}

std::vector<Quadro> simularChamadas(const std::vector<std::size_t>& chamadas) {
    const std::vector<ProcedimentoExemplo>& procs = procedimentosDoExemplo();
    std::vector<Quadro> pilha;

    for (const std::size_t proc : chamadas) {
        if (proc >= procs.size()) {
            continue;
        }
        Quadro q;
        q.procedimento = proc;

        if (!pilha.empty()) {
            q.temElo = true;
            // Cadeia de CONTROLE: sempre o quadro imediatamente abaixo, isto é,
            // quem chamou. Não depende de onde o procedimento foi escrito.
            q.eloDeControle = pilha.size() - 1;

            // Cadeia de ACESSO: o quadro mais recente cujo procedimento é o que
            // CONTÉM este no texto. Procura de cima para baixo — o mais
            // recente é o correto, e é isso que faz a recursão funcionar.
            const std::size_t envolvente = procs[proc].contidoEm;
            q.eloDeAcesso = pilha.size() - 1;
            for (std::size_t i = pilha.size(); i > 0; --i) {
                if (pilha[i - 1].procedimento == envolvente) {
                    q.eloDeAcesso = i - 1;
                    break;
                }
            }
        }
        pilha.push_back(q);
    }
    return pilha;
}

std::string desenharPilha(const std::vector<Quadro>& pilha) {
    const std::vector<ProcedimentoExemplo>& procs = procedimentosDoExemplo();
    std::ostringstream out;
    out << "    quadro | procedimento | nivel | controle | acesso\n";
    out << "    -------+--------------+-------+----------+-------\n";
    for (std::size_t i = 0; i < pilha.size(); ++i) {
        const Quadro& q = pilha[i];
        const ProcedimentoExemplo& p = procs[q.procedimento];
        out << "      " << i << "    | " << p.nome;
        for (std::size_t k = p.nome.size(); k < 12; ++k) {
            out << ' ';
        }
        out << " |   " << p.nivelLexico << "   |    ";
        if (q.temElo) {
            out << q.eloDeControle << "     |   " << q.eloDeAcesso;
        } else {
            out << "-     |   -";
        }
        out << '\n';
    }
    return out.str();
}

std::size_t resolverAcesso(const std::vector<Quadro>& pilha,
                           std::size_t nivelLexicoAlvo) {
    if (pilha.empty()) {
        return 0;
    }
    const std::vector<ProcedimentoExemplo>& procs = procedimentosDoExemplo();

    std::size_t atual = pilha.size() - 1;
    // Sobe pela cadeia de ACESSO até chegar ao nível léxico procurado. O número
    // de saltos é a diferença de níveis, conhecida na compilação — é por isso
    // que este percurso não custa busca em tempo de execução num compilador de
    // verdade.
    while (procs[pilha[atual].procedimento].nivelLexico > nivelLexicoAlvo) {
        if (!pilha[atual].temElo) {
            return pilha.size();
        }
        const std::size_t proximo = pilha[atual].eloDeAcesso;
        if (proximo == atual) {
            return pilha.size();
        }
        atual = proximo;
    }
    if (procs[pilha[atual].procedimento].nivelLexico == nivelLexicoAlvo) {
        return atual;
    }
    return pilha.size();
}

}  // namespace peneira

O exemplo tem três procedimentos aninhados: principal contém externo, que contém interno. Três níveis são o mínimo para as duas cadeias divergirem, e a divergência é o assunto inteiro. A demonstração --ativacoes simula a sequência principal chama externo, que chama interno, que chama externo de novo:

    quadro | procedimento | nivel | controle | acesso
    -------+--------------+-------+----------+-------
      0    | principal    |   0   |    -     |   -
      1    | externo      |   1   |    0     |   0
      2    | interno      |   2   |    1     |   1
      3    | externo      |   1   |    2     |   0

O quadro 3 é onde tudo se decide. A cadeia de controle aponta para o quadro 2, porque foi interno quem chamou — ela segue a ordem de execução. A cadeia de acesso aponta para o quadro 0, porque é principal que contém externo no texto do programa. As duas divergem, e é essa divergência que obriga a manter as duas: uma responde “para onde volto”, a outra responde “onde estão as variáveis que enxergo”.

A resolução de acesso a partir do topo confirma:

    variavel de nivel lexico 0 -> quadro 0 (principal)
    variavel de nivel lexico 1 -> quadro 3 (externo)
    variavel de nivel lexico 2 -> nao visivel daqui

O nível 2 não é visível do quadro 3, e está certo: interno está na pilha, mas não envolve externo no texto, então suas variáveis não são acessíveis dali. Confundir “está na pilha” com “é visível” é o erro que a cadeia de acesso existe para impedir.

Sobre gerência de memória, que a ementa também menciona: o objeto da Peneira aloca explicitamente e libera por escopo — o texto casado vive enquanto a regra executa e some ao terminar, o que é o padrão de região, o mais simples que existe. A coleta automática não foi implementada e não caberia: coletar exige rastrear referências que sobrevivem à ativação, e nesta linguagem nada sobrevive.

Onde é fácil errar aqui. Montar a cadeia de acesso apontando para o quadro do procedimento envolvente mais antigo em vez do mais recente. Com recursão, os dois diferem, e a versão errada acessa a variável da ativação errada — silenciosamente, porque a variável existe nos dois quadros.

Como verificar. O número de saltos na cadeia de acesso tem de ser igual à diferença de níveis léxicos entre quem acessa e quem declara. Essa diferença é conhecida na compilação, e é por isso que num compilador de verdade este percurso não custa busca em tempo de execução.

1.7 Verificação da entrega

Item Como conferir Estado nesta referência
Justificativa da representação Argumento aplicado ao caso, não copiado Combinatório calculado e descartado; justificativa real é a separação de dificuldades
Comparação das formas Mesma expressão nas três formas Árvore, pós-fixada e três endereços
Tradução implementada Árvore verificada vira código intermediário Duas ações traduzidas, 9 temporários
Rótulos e desvios Curto-circuito de and e de or Ambos, com traçado
Endereços desconhecidos Preenchimento retroativo 0 desvios pendentes ao fim, verificado
Especificação do formato Campos com semântica, não só nome Duas seções, quinze instruções
Modelo de execução Suficiente para implementação independente Laço, disparo, semântica das instruções, erros
Desempate especificado Comportamento definido quando dois padrões casam igual Ordem de declaração
Organização da memória Quatro áreas, com o que é fixo e o que cresce Medido: 5137 bytes estáticos
Registro de ativação Composição e protocolo de retorno Cinco campos, profundidade máxima 1
Cadeias de controle e acesso Divergência exibida, não afirmada Exemplo isolado, quadro 3 diverge
Especificação antes do código Ordem de trabalho registrada Cabeçalho escrito primeiro; revelou duas decisões
Compilação limpa Modo estrito, aviso como erro Sem nenhum diagnóstico

O que quero deixar registrado sobre esta entrega é o que a escrita da especificação produziu que a implementação não teria produzido. Duas decisões só existem porque o documento veio antes: a troca de representação da tabela de transição na fronteira entre compilador e objeto, e o desempate entre padrões de mesmo comprimento. A segunda é a mais reveladora — ela não é uma otimização nem um detalhe, é uma escolha que muda a saída do programa, e ela estava ausente de tudo que eu tinha escrito até aqui. Dois executores corretos, lidos os documentos anteriores, produziriam resultados diferentes para o mesmo programa. Foi o critério de completude, aplicado literalmente à minha própria especificação, que a fez aparecer.

E fica um número desconfortável de propósito. A tabela de transição do objeto usa quatro por cento do espaço que ocupa. A decisão continua sendo a certa pela razão que está escrita, mas ela agora tem um número medido ao lado, e não uma justificativa genérica — que é a diferença entre uma decisão de engenharia e uma preferência.