Trilha 18 · Lógica e teoria da computação

Gramáticas e linguagens

A hierarquia de Chomsky classifica linguagens por complexidade: regulares (DFA), livres de contexto (gramáticas CFG + autômatos com pilha), sensíveis ao contexto e recursivamente enumeráveis (Turing). A linguagem de programação que você usa hoje pertence à segunda camada — uma CFG define sua sintaxe.

① Intuição

Quando um DFA não basta: precisamos de memória

A linguagem aⁿbⁿ — strings com o mesmo número de 'a' e 'b' — não pode ser reconhecida por um DFA porque exige contar. Um DFA tem memória fixa (o estado atual), então não consegue representar "vi n 'a's e agora preciso ver exatamente n 'b's" para n arbitrário.

A solução é adicionar uma pilha: empilhamos um símbolo para cada 'a' e desempilhamos um para cada 'b'. Se a pilha esvaziar exatamente quando a entrada terminar, aceitamos. Esse modelo é o autômato com pilha (PDA), e ele reconhece exatamente as linguagens livres de contexto (CFL).

Hierarquia de Chomsky (1956): Tipo 3 (Regular) ⊂ Tipo 2 (Livre de contexto) ⊂ Tipo 1 (Sensível ao contexto) ⊂ Tipo 0 (Recursivamente enumerável). Quanto mais alto na hierarquia, mais poderoso o modelo — e mais caro computacionalmente. Linguagens de programação são Tipo 2 (com algumas extensões sensíveis ao contexto para verificação de tipos).
② Visualização interativa

PDA para aⁿbⁿ

Escolha o valor de n. O PDA empilha A para cada 'a' e desempilha para cada 'b'. A pilha deve ter apenas Z₀ ao final para aceitar.

n =entrada: aaabbb
ENTRADA
a
a
a
b
b
b
ESTADO
q₀
restante: aaabbb
STACK ↑
Z₀
Início. Stack: só Z₀ (marcador de fundo). Vamos empilhar um A para cada 'a'.
1/7
③ Explicação técnica

Gramáticas livres de contexto (CFG)

# Gramática livre de contexto (CFG) para aⁿbⁿ
# Notação BNF

S → a S b   # regra recursiva: envolve S com a e b
S → ε       # caso base: string vazia

# Derivações:
S ⇒ aSb ⇒ aaSbb ⇒ aaaSbbb ⇒ aaabbb    # n=3
S ⇒ ε                                    # n=0

# Gramática para expressões aritméticas simples:
E → E + T | T
T → T * F | F
F → ( E ) | num

# Derivação de "3 * (2 + 1)":
E ⇒ T ⇒ T*F ⇒ F*F ⇒ num*F ⇒ num*(E) ⇒ num*(T+T) ⇒ ...

# Árvore de derivação (parse tree) — estrutura do parser
#          E
#          |
#          T
#        / | \
#       T  *  F
#       |   / | \
#       F  ( E   )
#       |    |
#      num  E+T → ...

Autômato com pilha (PDA) em Python

# Autômato com pilha (PDA) para aⁿbⁿ em Python
# Simulação manual — usa uma pilha explícita

def aceita_anbn(string):
    pilha = ["Z"]   # Z = marcador de fundo
    estado = "q0"

    for ch in string:
        if estado == "q0":
            if ch == "a":
                pilha.append("A")      # empilha A para cada 'a'
            elif ch == "b":
                if pilha[-1] != "A":
                    return False    # mais 'b' que 'a'
                pilha.pop()
                estado = "q1"
            else:
                return False
        elif estado == "q1":
            if ch == "b":
                if pilha[-1] != "A":
                    return False
                pilha.pop()
            else:
                return False

    return pilha == ["Z"]  # stack vazia (exceto Z) → aceita

aceita_anbn("aabb")    # True
aceita_anbn("aaabbb")  # True
aceita_anbn("aab")     # False
aceita_anbn("ab")      # True
Parser LL vs LR: compiladores analisam código-fonte usando algoritmos de parsing para CFGs. Parsers LL (top-down, recursivo descendente) são simples de implementar manualmente. Parsers LR (bottom-up, dirigido por tabela) são mais poderosos e usados por yacc/bison. O GCC usa um parser recursivo descendente escrito à mão; o LLVM usa um parser Pratt customizado para melhor diagnóstico de erros.
④ Projeto para programar

Construindo um parser

Mini projeto: escreva um parser recursivo descendente para expressões aritméticas com +, * e parênteses (sem variáveis). Use a gramática: E → T ('+' T)*, T → F ('*' F)*, F → num | '(' E ')'. Retorne o valor calculado diretamente.

Projeto principal: estenda o parser para construir uma árvore de sintaxe abstrata (AST) em vez de avaliar diretamente. Implemente: parser → AST → visitor de avaliação. Adicione suporte a variáveis (x = 3 + 2) usando um dicionário de ambiente. Gere a representação textual da AST (pretty-print com indentação).

Desafio extra: implemente um parser LR(1) a partir de uma gramática arbitrária. Construa as tabelas ACTION e GOTO automaticamente. Teste com gramáticas ambíguas (como expressões sem precedência definida) e observe os conflitos shift/reduce. Compare a velocidade com o parser recursivo descendente para arquivos grandes.

⑤ Exercícios rápidos

Teste sua intuição

Por que um DFA não consegue reconhecer aⁿbⁿ, e o que precisamos adicionar?
Qual é a relação entre gramáticas livres de contexto e linguagens de programação?
Uma propriedade importante das linguagens livres de contexto:
⑥ Aplicações no mundo real

Onde você encontra isso

🐍

CPython — o parser do Python

Até Python 3.8, o parser do CPython era um parser LL(1) gerado por pgen a partir da gramática do Python. Na 3.9+, foi substituído por um parser PEG (Parsing Expression Grammar) escrito à mão, mais expressivo. A gramática formal do Python está em Grammar/python.gram no repositório do CPython — 400 linhas descrevendo toda a sintaxe da linguagem.

🌐

HTML — gramática não livre de contexto

Curiosamente, HTML não é livre de contexto: a validação de tags aninhadas exige uma pilha, mas o tratamento de erros da especificação HTML5 vai além (considere tags opcionais como <p> sem fechamento). Navegadores implementam parsers customizados, não geradores de parser como yacc. O spec do HTML5 define o parser como uma máquina de estados com 80+ estados — equivalente a um PDA com lógica especial.

🔧

JSON / Protocol Buffers — parsing eficiente

JSON tem uma gramática CFG simples e bem definida (RFC 8259). Parsers de JSON de alta performance (simdjson) usam SIMD para processar 4 GB/s de JSON. Protocol Buffers usa uma gramática binária ainda mais simples — encoding de comprimento-valor sem necessidade de backtracking. Escolher a gramática certa afeta diretamente a velocidade do parser em produção.

← Anterior: Regex e autômatos Próxima: Máquina de Turing →