Moacyr Francischetti Corrêa

1 Módulo 06: Projeto do Professor — Onde a Ferramenta Para

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. A construção não regular da sua linguagem será outra, mas a forma de demonstrar e de confrontar é a mesma.

1.1 Visão Geral do Módulo 06

Cinco módulos construindo um motor de reconhecimento de padrões, e este serve para descobrir o que ele não consegue fazer. Não é um módulo de recuo: é o que justifica tudo que vem depois. Sem a impossibilidade demonstrada aqui, o autômato de pilha do módulo 9 e o analisador sintático do módulo 10 seriam técnicas alternativas em vez de necessidades.

A atividade pede três coisas encadeadas. Uma demonstração formal de que alguma construção da própria linguagem não é regular. Um experimento com a ferramenta do módulo 5, documentando exatamente como ela falha. E a articulação entre as duas — que é o que se avalia, porque o experimento sozinho é anedota e a prova sozinha é abstração.

A construção que escolhi é a que o contrato de capacidades exige de toda linguagem do projeto: o aninhamento de profundidade arbitrária. Na Peneira, ela aparece nas condições compostas do where, em que uma comparação pode conter outra entre parênteses sem limite de profundidade. Reduzida ao essencial, é a linguagem dos parênteses balanceados.

Além da atividade, o módulo tem um tópico teórico que admite implementação e que resolvi tratar como código de primeira classe: as propriedades de fechamento. Elas não são curiosidade — são ferramenta de prova, e implementá-las dá ao projeto a capacidade de decidir inclusão e equivalência entre linguagens, coisa que nenhum módulo anterior conseguia.

1.2 Tarefa 1: As propriedades de fechamento

O tópico — a classe das linguagens regulares é fechada sob união, interseção, complemento e diferença, e cada afirmação vem com a construção que a demonstra.

Implementei as quatro. A razão de fazê-lo antes do lema do bombeamento, e não depois, é que elas são instrumento de demonstração: mostrar que uma linguagem não é regular passa, com frequência, por combiná-la com uma regular conhecida e chegar a um absurdo.

06_fechamento.h
#ifndef PENEIRA_06_FECHAMENTO_H
#define PENEIRA_06_FECHAMENTO_H

#include <string>
#include <vector>

#include "03_afd.h"

namespace peneira {

// Propriedades de fechamento da classe das linguagens regulares, na forma das
// construções que as demonstram.
//
// Não são curiosidade teórica: são ferramenta de prova. Demonstrar que uma
// linguagem não é regular passa, com frequência, por combiná-la com uma
// regular conhecida — se a combinação resultasse regular e sabemos que não é,
// a hipótese cai. É por isso que estas construções aparecem antes do lema do
// bombeamento, e não depois.
//
// Todas recebem o alfabeto explicitamente. Complemento e união dependem de
// função de transição TOTAL, e totalizar exige saber sobre quais símbolos.
// Deixar o alfabeto implícito aqui produziria resultados errados de um jeito
// difícil de notar: o complemento sairia certo para os símbolos que o autômato
// já usava e errado para os demais.

// Torna a função de transição total, acrescentando um estado de erro
// absorvente quando necessário.
Afd completar(const Afd& a, const std::vector<Simbolo>& alfabeto,
              std::string nome);

// Complemento: mesma estrutura, finalidade invertida. Só funciona sobre
// autômato completo — sem o estado de erro explícito, as cadeias que caíam
// fora do autômato continuariam caindo fora em vez de passarem a ser aceitas.
Afd complementar(const Afd& a, const std::vector<Simbolo>& alfabeto,
                 std::string nome);

// Interseção pela construção do produto: os estados do resultado são pares de
// estados dos dois autômatos, e a cadeia é aceita quando os dois aceitam.
Afd interseccao(const Afd& a, const Afd& b,
                const std::vector<Simbolo>& alfabeto, std::string nome);

// União, também pelo produto: mesma construção, aceita quando qualquer um dos
// dois aceita.
Afd uniaoAfd(const Afd& a, const Afd& b, const std::vector<Simbolo>& alfabeto,
             std::string nome);

// Diferença: interseção com o complemento do segundo.
Afd diferencaAfd(const Afd& a, const Afd& b,
                 const std::vector<Simbolo>& alfabeto, std::string nome);

// Alfabeto formado pela união dos símbolos usados pelos dois autômatos.
std::vector<Simbolo> alfabetoComum(const Afd& a, const Afd& b);

// A linguagem reconhecida é vazia? Verdadeiro quando nenhum estado final é
// alcançável. Combinado com a diferença, decide inclusão de linguagens; com a
// diferença simétrica, decide equivalência.
bool linguagemVazia(const Afd& a);

}  // namespace peneira

#endif  // PENEIRA_06_FECHAMENTO_H
06_fechamento.cpp
#include "06_fechamento.h"

#include <map>
#include <set>
#include <utility>
#include <vector>

namespace peneira {

namespace {

using ParDeEstados = std::pair<Estado, Estado>;

// Produto de dois autômatos. O predicado decide, a partir da finalidade de
// cada lado, se o estado do produto é final — é o único ponto em que interseção
// e união diferem, e por isso a construção é escrita uma vez só.
Afd produto(const Afd& a, const Afd& b, const std::vector<Simbolo>& alfabeto,
            std::string nome, bool exigirAmbos) {
    const Afd ca = completar(a, alfabeto, a.nome());
    const Afd cb = completar(b, alfabeto, b.nome());

    Afd resultado(std::move(nome));
    if (ca.inicial() == kSemEstado || cb.inicial() == kSemEstado) {
        return resultado;
    }

    std::map<ParDeEstados, Estado> conhecidos;
    std::vector<ParDeEstados> fila;

    const ParDeEstados inicio{ca.inicial(), cb.inicial()};
    const bool inicioFinal =
        exigirAmbos ? (ca.ehFinal(inicio.first) && cb.ehFinal(inicio.second))
                    : (ca.ehFinal(inicio.first) || cb.ehFinal(inicio.second));
    conhecidos[inicio] = resultado.novoEstado(inicioFinal);
    resultado.definirInicial(conhecidos[inicio]);
    fila.push_back(inicio);

    // Só os pares alcançáveis são criados. O produto completo teria o produto
    // dos tamanhos; na prática a parte alcançável costuma ser bem menor.
    for (std::size_t i = 0; i < fila.size(); ++i) {
        const ParDeEstados atual = fila[i];
        const Estado origem = conhecidos[atual];

        for (const Simbolo s : alfabeto) {
            const Estado da = ca.transicao(atual.first, s);
            const Estado db = cb.transicao(atual.second, s);
            if (da == kSemEstado || db == kSemEstado) {
                continue;
            }

            const ParDeEstados destino{da, db};
            const auto it = conhecidos.find(destino);
            Estado indice;
            if (it == conhecidos.end()) {
                const bool final =
                    exigirAmbos ? (ca.ehFinal(da) && cb.ehFinal(db))
                                : (ca.ehFinal(da) || cb.ehFinal(db));
                indice = resultado.novoEstado(final);
                conhecidos[destino] = indice;
                fila.push_back(destino);
            } else {
                indice = it->second;
            }
            resultado.adicionarTransicao(origem, s, indice);
        }
    }
    return resultado;
}

}  // namespace

Afd completar(const Afd& a, const std::vector<Simbolo>& alfabeto,
              std::string nome) {
    Afd resultado(std::move(nome));
    for (std::size_t e = 0; e < a.quantidadeDeEstados(); ++e) {
        resultado.novoEstado(a.ehFinal(e));
    }
    const Estado erro = resultado.novoEstado(false);
    if (a.inicial() != kSemEstado) {
        resultado.definirInicial(a.inicial());
    } else {
        resultado.definirInicial(erro);
    }

    for (std::size_t e = 0; e < a.quantidadeDeEstados(); ++e) {
        for (const Simbolo s : alfabeto) {
            const Estado destino = a.transicao(e, s);
            resultado.adicionarTransicao(e, s,
                                         destino == kSemEstado ? erro : destino);
        }
    }
    for (const Simbolo s : alfabeto) {
        resultado.adicionarTransicao(erro, s, erro);
    }
    return resultado;
}

Afd complementar(const Afd& a, const std::vector<Simbolo>& alfabeto,
                 std::string nome) {
    const Afd completo = completar(a, alfabeto, a.nome());

    Afd resultado(std::move(nome));
    for (std::size_t e = 0; e < completo.quantidadeDeEstados(); ++e) {
        // A inversão acontece aqui, e só faz sentido porque o autômato está
        // completo: o estado de erro, que era não final, passa a ser final, e
        // é ele que aceita as cadeias que o original rejeitava por falta de
        // transição.
        resultado.novoEstado(!completo.ehFinal(e));
    }
    resultado.definirInicial(completo.inicial());

    for (std::size_t e = 0; e < completo.quantidadeDeEstados(); ++e) {
        for (const Simbolo s : alfabeto) {
            const Estado destino = completo.transicao(e, s);
            if (destino != kSemEstado) {
                resultado.adicionarTransicao(e, s, destino);
            }
        }
    }
    return resultado;
}

Afd interseccao(const Afd& a, const Afd& b,
                const std::vector<Simbolo>& alfabeto, std::string nome) {
    return produto(a, b, alfabeto, std::move(nome), true);
}

Afd uniaoAfd(const Afd& a, const Afd& b, const std::vector<Simbolo>& alfabeto,
             std::string nome) {
    return produto(a, b, alfabeto, std::move(nome), false);
}

Afd diferencaAfd(const Afd& a, const Afd& b,
                 const std::vector<Simbolo>& alfabeto, std::string nome) {
    const Afd naoB = complementar(b, alfabeto, "complemento");
    return interseccao(a, naoB, alfabeto, std::move(nome));
}

std::vector<Simbolo> alfabetoComum(const Afd& a, const Afd& b) {
    std::set<Simbolo> juntos;
    for (const Simbolo s : a.simbolosUsados()) juntos.insert(s);
    for (const Simbolo s : b.simbolosUsados()) juntos.insert(s);
    return std::vector<Simbolo>(juntos.begin(), juntos.end());
}

bool linguagemVazia(const Afd& a) {
    if (a.inicial() == kSemEstado) {
        return true;
    }
    const std::vector<Simbolo> alfabeto = a.simbolosUsados();
    std::set<Estado> vistos{a.inicial()};
    std::vector<Estado> pilha{a.inicial()};

    while (!pilha.empty()) {
        const Estado atual = pilha.back();
        pilha.pop_back();
        if (a.ehFinal(atual)) {
            return false;
        }
        for (const Simbolo s : alfabeto) {
            const Estado destino = a.transicao(atual, s);
            if (destino != kSemEstado && vistos.insert(destino).second) {
                pilha.push_back(destino);
            }
        }
    }
    return true;
}

}  // namespace peneira

Três decisões de projeto sustentam essas construções.

O alfabeto é parâmetro explícito, não inferido. Esta foi a decisão mais importante e a menos óbvia. Complemento e união dependem de função de transição total, e totalizar exige saber sobre quais símbolos. Se eu deixasse o alfabeto implícito, tomando os símbolos que o autômato já usa, o complemento sairia certo para esses símbolos e errado para todos os outros — a cadeia xyz sobre um autômato que só conhece a e b deveria ser aceita pelo complemento e não seria. Erro silencioso, do tipo que passa em qualquer teste que use apenas o alfabeto original.

A união e a interseção são o mesmo código. As duas são a construção do produto, e diferem em uma linha: o estado do produto é final quando os dois lados são finais, ou quando qualquer um é. Escrevi a construção uma vez, com um parâmetro decidindo o predicado. Duplicar seria convidar as duas versões a divergirem na primeira manutenção.

Só os pares alcançáveis são criados. O produto completo teria o produto dos tamanhos — quatro estados de um lado e cinco do outro dariam vinte. Construindo por percurso a partir do par inicial, só nascem os pares que o autômato realmente atinge. Nos exemplos deste módulo, a interseção de um autômato de três estados com um de dois resulta em três, não em seis.

A demonstração confere as quatro construções contra as definições, cadeia por cadeia:

cadeia  L1    L2    L1 e L2  L1 ou L2  nao L1   L2-L1
ab      sim   sim   sim      sim       nao      nao
a       nao   sim   nao      sim       sim      sim
b       nao   nao   nao      nao       sim      nao
<vazia> nao   nao   nao      nao       sim      nao

  as construcoes conferem com as definicoes? sim

Repare que a verificação não olha o resultado esperado escrito à mão: ela calcula o que a definição de cada operação exige a partir dos dois autômatos originais e compara. Uma tabela de valores esperados escrita por mim teria os mesmos erros que a implementação, se eu tivesse entendido a operação errado.

O que isso destrava. Combinando diferença com o teste de linguagem vazia, o projeto passa a decidir inclusão: a linguagem de A está contida na de B exatamente quando a diferença é vazia. E combinando as duas diferenças, decide equivalência. A demonstração fecha com o caso mais simples — a interseção de uma linguagem com o próprio complemento é vazia, e tem de ser.

1.2.1 Fechamento como ferramenta de prova

O uso que justifica implementar tudo isso não é construir linguagens novas — é demonstrar que uma linguagem não é regular sem precisar bombear. O raciocínio tem uma elegância que vale mostrar, e é o motivo de o módulo tratar fechamento antes do lema.

Suponha que eu queira provar que a linguagem das cadeias com igual número de a e de b não é regular. Bombear diretamente sobre ela dá trabalho, porque o adversário tem muitas decomposições à disposição. O atalho é intersectá-la com a*b*, que é regular e cujo autômato é trivial. O resultado da interseção é exatamente a linguagem das cadeias com n letras a seguidas de n letras b.

Agora o argumento se fecha sozinho. A classe é fechada sob interseção: se a linguagem original fosse regular, a interseção com uma regular também seria. Mas a interseção é a linguagem canônica que sabemos não ser regular. Contradição, e a hipótese cai. Nenhum bombeamento foi feito sobre a linguagem original — ele foi feito uma vez, sobre o caso canônico, e reaproveitado por transferência.

O mesmo padrão se aplica ao complemento, com uma consequência que costuma surpreender: se o complemento de uma linguagem não é regular, a linguagem também não é. Como a classe é fechada sob complemento, uma das duas ser regular obrigaria a outra a ser. Isso permite escolher, entre a linguagem e o complemento dela, aquela sobre a qual a demonstração é mais fácil — e às vezes a diferença de dificuldade é grande.

No projeto, a utilidade imediata é outra e é prática: com diferença e teste de vazio implementados, passo a poder verificar automaticamente se uma categoria léxica está contida em outra. Se a linguagem de IDENTIFICADOR estivesse contida na de PALAVRA_RESERVADA, ou vice-versa, isso indicaria um problema de especificação que hoje só apareceria no módulo 7, quando o desempate por prioridade fosse implementado. A ferramenta existe antes do problema.

Onde é fácil errar aqui. Inverter a finalidade sem completar antes. O complemento de um autômato esparso, feito trocando finais por não finais, produz um autômato que rejeita as cadeias que caem fora — quando deveria aceitá-las, porque o original as rejeitava. O estado de erro precisa existir explicitamente para poder virar final.

1.3 Tarefa 2: O lema do bombeamento, exibido rodando

O tópico — toda cadeia suficientemente longa aceita por um autômato finito pode ser decomposta e bombeada.

O lema costuma ser ensinado direto na forma contrapositiva, que é como ele se usa. Resolvi mostrá-lo antes na forma direta, sobre uma linguagem que é regular, porque quem viu a decomposição funcionando acredita nela depois, quando ela for usada para derivar contradição.

06_bombeamento.h
#ifndef PENEIRA_06_BOMBEAMENTO_H
#define PENEIRA_06_BOMBEAMENTO_H

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

#include "02_cadeia.h"
#include "03_afd.h"

namespace peneira {

// A decomposição que o lema do bombeamento garante existir: toda cadeia aceita
// com comprimento maior ou igual ao número de estados pode ser escrita como
// x y z, com y não vazio, |xy| dentro do número de estados, e x y^i z aceita
// para todo i.
struct Decomposicao {
    Cadeia x;
    Cadeia y;
    Cadeia z;
    Estado repetido = kSemEstado;
    std::size_t inicioDoCiclo = 0;
    std::size_t fimDoCiclo = 0;
};

// Encontra a decomposição percorrendo a cadeia e anotando o estado alcançado
// após cada prefixo. Com mais símbolos lidos do que estados existentes, algum
// estado necessariamente se repete — é o princípio da casa dos pombos, e é a
// demonstração inteira do lema.
//
// Devolve vazio quando a cadeia é curta demais para forçar repetição, ou
// quando não é aceita.
std::optional<Decomposicao> decompor(const Afd& a, const Cadeia& s);

// Gera x y^i z para i de 0 até o limite. Com i igual a 1 reproduz a cadeia
// original; com 0, remove o trecho repetido.
std::vector<Cadeia> bombear(const Decomposicao& d, std::size_t ate);

// Constrói a notação de uma expressão regular que reconhece os parênteses
// balanceados até a profundidade dada — e apenas até ela.
//
// É a tentativa que todo grupo faz antes de aceitar que o modelo não dá conta:
// como não é possível cobrir profundidade arbitrária, cobre-se até k. A
// expressão cresce, o autômato cresce, e sempre existe k+1.
std::string notacaoParentesesAte(std::size_t profundidade);

// Cadeia de parênteses aninhados na profundidade dada: "((( )))" sem espaços.
Cadeia parentesesNaProfundidade(std::size_t profundidade);

}  // namespace peneira

#endif  // PENEIRA_06_BOMBEAMENTO_H
06_bombeamento.cpp
#include "06_bombeamento.h"

#include <map>
#include <utility>

namespace peneira {

std::optional<Decomposicao> decompor(const Afd& a, const Cadeia& s) {
    if (a.inicial() == kSemEstado || !a.aceita(s)) {
        return std::nullopt;
    }
    const std::size_t n = a.quantidadeDeEstados();
    if (s.size() < n) {
        // Cadeia curta demais: sem forçar repetição, o lema nada garante.
        return std::nullopt;
    }

    // Anota o estado alcançado após cada prefixo. A primeira repetição dentro
    // dos primeiros n passos é o ciclo procurado — e ela existe porque há n+1
    // posições anotadas para no máximo n estados distintos.
    std::map<Estado, std::size_t> primeiraOcorrencia;
    Estado atual = a.inicial();
    primeiraOcorrencia[atual] = 0;

    for (std::size_t i = 0; i < s.size() && i < n; ++i) {
        atual = a.transicao(atual, static_cast<Simbolo>(s[i]));
        if (atual == kSemEstado) {
            return std::nullopt;
        }
        const auto it = primeiraOcorrencia.find(atual);
        if (it != primeiraOcorrencia.end()) {
            Decomposicao d;
            d.inicioDoCiclo = it->second;
            d.fimDoCiclo = i + 1;
            d.repetido = atual;
            d.x = s.substr(0, d.inicioDoCiclo);
            d.y = s.substr(d.inicioDoCiclo, d.fimDoCiclo - d.inicioDoCiclo);
            d.z = s.substr(d.fimDoCiclo);
            return d;
        }
        primeiraOcorrencia[atual] = i + 1;
    }
    return std::nullopt;
}

std::vector<Cadeia> bombear(const Decomposicao& d, std::size_t ate) {
    std::vector<Cadeia> resultado;
    resultado.reserve(ate + 1);
    for (std::size_t i = 0; i <= ate; ++i) {
        resultado.push_back(d.x + potencia(d.y, i) + d.z);
    }
    return resultado;
}

std::string notacaoParentesesAte(std::size_t profundidade) {
    // Alternativa por profundidade: \(\) | \(\(\)\) | \(\(\(\)\)\) | ...
    // Os parênteses precisam de barra invertida porque na notação eles são
    // agrupamento, não símbolo.
    std::string notacao;
    for (std::size_t k = 1; k <= profundidade; ++k) {
        if (k > 1) {
            notacao += "|";
        }
        for (std::size_t i = 0; i < k; ++i) notacao += "\\(";
        for (std::size_t i = 0; i < k; ++i) notacao += "\\)";
    }
    return notacao;
}

Cadeia parentesesNaProfundidade(std::size_t profundidade) {
    Cadeia s;
    for (std::size_t i = 0; i < profundidade; ++i) s += '(';
    for (std::size_t i = 0; i < profundidade; ++i) s += ')';
    return s;
}

}  // namespace peneira

A implementação é a demonstração do lema transcrita. Percorro a cadeia anotando o estado alcançado após cada prefixo. Com mais símbolos lidos do que estados existentes, há mais anotações do que valores possíveis, e algum estado necessariamente se repete — princípio da casa dos pombos. A primeira repetição delimita o ciclo, e o ciclo é o y que pode ser percorrido quantas vezes se queira.

Rodando sobre o autômato do número, que tem cinco estados, com a cadeia 123456:

  estado repetido: q2 (apos 1 e apos 2 simbolos)
  x = "1"
  y = "2"   (nao vazio, como o lema exige)
  z = "3456"
  |xy| = 2, dentro dos 5 estados

  bombeando:
    i=0  "13456"     -> aceita
    i=1  "123456"    -> aceita
    i=2  "1223456"   -> aceita
    i=3  "12223456"  -> aceita
    i=4  "122223456" -> aceita

As três condições do lema aparecem na saída e podem ser conferidas: o y não é vazio, o comprimento de xy cabe dentro do número de estados, e todas as potências são aceitas. O caso i = 0 merece atenção porque é o que mais confunde: bombear com zero remove o trecho, e a cadeia encurtada também precisa ser aceita.

A estrutura lógica, que é onde as demonstrações erram. O lema diz que existe uma decomposição, escolhida pelo adversário, tal que para todo i a cadeia bombeada é aceita. Ao usá-lo na forma contrapositiva, quem escolhe a cadeia é você, quem escolhe a decomposição é o adversário, e você precisa exibir um i que produza cadeia fora da linguagem — para qualquer decomposição que ele escolha.

A inversão dessa ordem é o erro dominante. Escolher a decomposição em vez de tratá-la como dada produz uma demonstração que parece completa e não é. O código deixa a assimetria visível: a função de decomposição encontra a decomposição, ela não a recebe — porque no lema quem a fornece é o autômato, não quem demonstra.

Onde é fácil errar aqui. Procurar a repetição na cadeia inteira em vez de nos primeiros n passos. Se você aceitar qualquer repetição, o y encontrado pode ficar além do limite que o lema garante, e a condição sobre o comprimento de xy deixa de valer. O laço para no número de estados de propósito.

1.4 Tarefa 3: O experimento — a ferramenta diante do aninhamento

A atividade — confrontar a ferramenta construída até o módulo 5 com uma construção aninhada e documentar como e por que ela falha.

Aqui o experimento reproduz o que todo grupo faz antes de aceitar o resultado teórico: se não dá para cobrir profundidade arbitrária, cobre-se até uma profundidade k. A notação suporta alternativa, então basta enumerar — parênteses até um nível, ou até dois, ou até três.

Rodando o pipeline completo do módulo 5 sobre essa expressão, para k de 1 a 8:

   k   estados      aceita k   aceita k+1
   1         3           sim           nao
   2         5           sim           nao
   3         7           sim           nao
   4         9           sim           nao
   5        11           sim           nao
   6        13           sim           nao
   7        15           sim           nao
   8        17           sim           nao

Três leituras dessa tabela, em ordem crescente de importância.

A primeira é que funciona — para cada k, o autômato aceita corretamente a profundidade k. Não há defeito de implementação; a ferramenta faz o que promete.

A segunda é o crescimento: os estados seguem 2k + 1, linear em k. Cada nível de profundidade cobrível custa dois estados. Isso é a memória do autômato sendo gasta para contar, e contar é exatamente o que um autômato finito não pode fazer sem limite.

A terceira é a coluna da direita, que é a resposta: sempre falha em k+1. E como para todo k existe k+1, nenhum valor resolve. O autômato precisaria de infinitos estados, e “autômato finito com infinitos estados” é contradição em termos.

O que o experimento prova, e o que não prova. Ele não prova nada. Mostrar que oito tentativas falharam não exclui a nona — talvez exista uma expressão esperta que ninguém pensou. O que o experimento faz é revelar o padrão: a falha é sempre no primeiro nível além do coberto, e o custo em estados cresce com o alcance.

A demonstração formal é que transforma padrão em impossibilidade. Supondo que a linguagem fosse regular, existiria um autômato com algum número n de estados. Tome a cadeia com n aberturas seguidas de n fechamentos: ela é aceita e tem comprimento maior que n, então o lema garante uma decomposição com y não vazio dentro dos primeiros n símbolos — que são todos aberturas. Bombear com i = 2 acrescenta aberturas sem acrescentar fechamentos, e a cadeia resultante está desbalanceada. Contradição: o autômato teria de aceitar uma cadeia que não pertence à linguagem. Logo não existe tal autômato.

Prova e experimento fazem coisas diferentes e se sustentam mutuamente. O experimento mostra como falha e dá a intuição do porquê; a prova mostra que sempre falhará. Entregar só um dos dois é entregar metade.

Onde é fácil errar aqui. Concluir do experimento que “a ferramenta tem um limite que poderia ser aumentado”. O limite não é da implementação — não é o número de estados que cabe na memória, nem uma restrição da notação. É do modelo. Nenhuma implementação de autômato finito, em nenhuma linguagem, com qualquer quantidade de memória, resolve isto.

1.5 Referência teórica: a caracterização por classes de equivalência

O módulo apresenta, em tratamento conceitual, uma segunda forma de caracterizar as linguagens regulares: uma linguagem é regular exatamente quando o número de classes de prefixos indistinguíveis é finito. Dois prefixos são distinguíveis quando existe um sufixo que completa um e não completa o outro.

Resolvi torná-la observável, porque ela dá o mesmo resultado do lema por um caminho que muitos estudantes acham mais natural — contar em vez de derivar contradição.

Para os parênteses balanceados, os prefixos de abertura são todos distinguíveis dois a dois: ( precisa de um fechamento para completar, (( precisa de dois, e nenhum sufixo completa os dois ao mesmo tempo. Como há infinitas profundidades, há infinitas classes.

No autômato construído até a profundidade k, isso aparece como contagem:

   k   estados   estados dos prefixos
   2         5                       2
   4         9                       4
   6        13                       6
   8        17                       8

Um estado distinto por profundidade, exatamente. O autômato é obrigado a gastar um estado por classe de equivalência que precise distinguir — e é essa correspondência, entre classes e estados, que o teorema formaliza. Como as classes são infinitas e os estados são finitos, não há autômato.

Vale notar a ligação com o módulo anterior: o autômato mínimo tem exatamente um estado por classe de equivalência. É por isso que ele é único, e é por isso que a minimização e esta caracterização são o mesmo fato visto de dois ângulos.

1.6 O que isso significa para o projeto

Fecho registrando a consequência concreta, que é o que a atividade pede na conclusão.

A parte da Peneira que não é regular é a condição composta do where, com parênteses aninhados de profundidade arbitrária. Tudo o mais na linguagem — identificadores, números, textos, padrões, pontuação — é regular e já está resolvido pelo motor pronto.

Isso define a divisão de trabalho do resto do semestre com precisão. O analisador léxico do módulo 7 fica com o que é regular, e é só isso que ele consegue fazer. A estrutura aninhada sobe para a análise sintática, que precisa de um modelo com memória de profundidade arbitrária — a pilha, que chega no módulo 9 como autômato de pilha e no módulo 10 como analisador de verdade.

A separação entre análise léxica e sintática, que no módulo 1 foi apresentada como decomposição clássica e podia parecer convenção, tem agora uma justificativa demonstrada: são fases separadas porque exigem modelos de poder diferente, e o mais fraco dos dois é comprovadamente incapaz de fazer o trabalho do outro.

1.7 Verificação da entrega

Item Como conferir Estado nesta referência
Propriedades de fechamento Construções conferidas contra as definições, cadeia a cadeia Quatro operações, todas coerentes
Alfabeto explícito Complemento correto fora dos símbolos originais Parâmetro obrigatório nas quatro
Decisão de linguagem vazia Interseção de uma linguagem com o próprio complemento Vazia, como tem de ser
Lema na forma direta Decomposição encontrada e potências aceitas y não vazio, \lvert xy \rvert \le n, i de 0 a 5 aceitos
Experimento do aninhamento Aceita em k, falha em k+1, para k de 1 a 8 Confirmado; estados crescem como 2k+1
Demonstração formal Estrutura contrapositiva completa, com a escolha do adversário respeitada Escrita na tarefa 3
Articulação prova × experimento O texto diz o que cada um estabelece e o que não estabelece Bloco dedicado
Classes de equivalência Contagem de estados distintos por profundidade Um por profundidade, confirmado
Código compila limpo Nenhum aviso sob o modo estrito Atende, verificado por compilação e execução

O que quero deixar registrado sobre esta entrega é que ela é a única do semestre cujo resultado principal é uma impossibilidade. O código escrito aqui não acrescenta capacidade ao compilador — acrescenta conhecimento sobre o que a capacidade existente não alcança, e é esse conhecimento que torna o próximo bloco necessário em vez de arbitrário.