λseteclassesCOMPILADOR 01 Linguagens & Paradigmas · Insper AEspaço da Adrielle
Sete classes, quatro roteiros

Sete Classes,
Quatro Roteiros

Duas metades que se explicam: a teoria — gramáticas, autômatos, o lema do bombeamento e as árvores — e a implementação, roteiro a roteiro, com o código real de cada versão e um depurador para você escrever a expressão que quiser e ver o compilador executá-la passo a passo.

v0.0 Roteiro 1 3 classes nascem v1.0 Roteiro 2 3 classes mesma quantidade, outro formato v1.1 Roteiro 3 3 classes +2 métodos no Parser v1.2 Roteiro 4 7 classes +4 de nó
A contagem só sobe no último roteiro. Os dois do meio não acrescentam classes — reorganizam as que já existiam.
Comece por aqui

Oficina de diagramas sintáticos

Como esta parte funciona

Os diagramas não são para decorar — são para ler.

Todo diagrama da disciplina é feito de seis peças, e cada peça vira sempre o mesmo pedaço de código. Aprenda as seis, e qualquer diagrama que aparecer na prova você lê como um programa. Depois, refazemos o caminho que o professor faz na Aula 6: começamos com um diagrama só, vemos ele dar a resposta errada, e consertamos — que é o único jeito de entender por que existem três.

1

O alfabeto: as seis peças

É a legenda que o professor apresenta na Aula 6. Repare no círculo (consumir um token) e no retângulo (chamar outro diagrama) — confundir os dois é a origem de quase toda dificuldade com esses diagramas. A bolinha vermelha mostra por onde dá para passar.

elemento (1) Fluxo direto sem operaçãoSó leva de um ponto ao outro. Não consome nada, não decide nada.
# nada: é só a ordem das instruções
a
elemento (2) Consumo de um token aUm círculo: o token do momento precisa ser a. Se for, consome e segue; se não, é erro de sintaxe.
if next.type != "A":
    raise Exception(...)
lexer.select_next()
ab
elemento (3) Condicional: consumo de a ou bO trilho se divide. O token atual decide o caminho — nunca os dois.
if next.type == "A":
    ...
elif next.type == "B":
    ...
else:
    raise Exception(...)
X
elemento (4) Direcionar para outro DS (chamada)Um retângulo: vá percorrer o diagrama X inteiro, e volte aqui com o que ele devolver.
resultado = parse_x()
a
elemento (5) Repetição com 1 ou mais vezesO elemento está no trilho principal — passa por ele pelo menos uma vez. A volta por baixo permite repetir.
consome a
while next.type == "A":
    consome a
a
elemento (6) Repetição com 0 ou mais vezesO trilho principal passa reto. O elemento fica na volta — dá para pular de vez, ou repetir quantas vezes quiser.
while next.type == "A":
    consome a
A diferença entre (5) e (6) é onde o elemento mora

Em (5) o elemento está no trilho principal: você é obrigado a passar por ele uma vez, e a volta permite repetir. Em (6) o trilho principal é reto: o elemento está na volta, então dá para nunca passar por ele. Quando você olhar o diagrama do EXPRESSION, veja que o TERM está no trilho principal e os operadores estão na volta — ou seja: pelo menos um TERM, e zero ou mais pares operador + TERM. Exatamente o que diz a EBNF: TERM, { ("+" | "-"), TERM }.

2

O caso que motiva tudo: 2 + 3 * 4

Antes de ver a solução, veja o problema. Se você põe todos os operadores no mesmo laço, a sintaxe continua perfeita — o diagrama aceita a cadeia. Mas ele calcula da esquerda para a direita:

Um diagrama só, com + − * / no laço = 20

2 + 3 = 5, depois 5 × 4 = 20. Aceita a cadeia, erra a conta.

Dois diagramas: EXPRESSION chama TERM = 14

O TERM resolve 3 × 4 = 12 antes de devolver. Só então 2 + 12.

3

A oficina: rastreie você mesmo

É o rastreamento que o professor desenha nos slides, agora andando. A linha vermelha tracejada é o caminho percorrido, com o número de cada passo. A fita verde em cima são os tokens, riscados conforme são consumidos. E — o mais importante — cada vez que um retângulo chama outro diagrama, aparece uma cópia nova dele, com o seu próprio resultado. É assim que dá para ver a recursão.

Expressão
⇓
—
—
Três coisas para observar enquanto roda

1. Na etapa tentativa errada, com 2+3*4, olhe o resultado mudar a cada volta do laço: 2, depois 5, depois 20. Nada impede o + de ser feito antes do *.

2. Na etapa dois níveis, o EXPRESSION não soma nada até o segundo TERM voltar. O TERM da 2ª chamada fica rodando o seu próprio laço — vê o *, pega o 4, calcula 12 — e só depois devolve. A precedência é esse “não devolver ainda”.

3. Na etapa gramática completa, tente (2+3)*4: repare no EXPRESSION que aparece dentro de um FACTOR. É o único diagrama que chama um diagrama “acima” dele — e é por isso que dá para aninhar parênteses sem limite.

4

Leia o diagrama como código

Os três diagramas finais, lado a lado com o main.py. Passe o mouse sobre uma peça do diagrama para ver a linha de código que ela vira — ou sobre uma linha, para ver de que peça ela veio.

A regra de tradução, em uma frase

Círculo vira select_next() (depois de conferir o tipo). Retângulo vira chamada de função. Bifurcação vira if/elif. Volta por baixo vira while. Com essas quatro, você escreve o parser de qualquer diagrama sem precisar pensar — e lê qualquer parser como diagrama.

Parte I

As aulas

Seis aulas que formam um arco: a teoria vira código, as linguagens regulares fecham, o parser ganha precedência, a linguagem sobe de classe, as árvores aparecem, e no fim vêm as estratégias de parsing e os limites formais.

04 a teoria vira código

Diagrama sintático, tokenização e o embrião do Parser

A frase que separa as duas fases — e que vale para o semestre inteiro

O Lexer decide "que token é este?". O Parser decide "essa sequência de tokens é válida?". Duas perguntas completamente diferentes, e é por isso que são duas classes.

1

A linguagem de partida

Uma calculadora mínima: só + e -, com números de vários dígitos. São válidas:

1
1+1
1+2+3
11-22-33

A gramática que descreve isso cabe numa linha:

EXPRESSION → NUMBER { ( "+" | "-" ) NUMBER }

E a pergunta da aula é: como transformar essa regra em algo fácil de implementar?

2

O diagrama sintático

É uma terceira forma de representar a mesma gramática — e a mais próxima do código que virá depois. Em vez de símbolos, um trilho: você entra pela esquerda, percorre os caminhos possíveis e sai pela direita. Toda rota completa é uma cadeia válida.

NUMBER saída + ou − o laço volta para ler outro NUMBER seguir o trilho até a saída = cadeia válida
O mesmo que EXPRESSION → NUMBER { ("+"|"-") NUMBER }. Repare que o laço é opcional e pode ser percorrido quantas vezes quiser: é exatamente o que as chaves da EBNF significam.
Por que desenhar, se a EBNF já dizia a mesma coisa

Porque o diagrama já tem o formato do código. O trilho principal vira a sequência de instruções; o laço vira um while; a bifurcação entre + e - vira um if. Você não "traduz" a gramática: você copia o desenho.

3

Primeira responsabilidade: o Lexer

O Lexer recebe caracteres e devolve tokens — os "átomos" do código-fonte. Ele agrupa 1 e 2 num único INT(12), descarta espaços, e não tem opinião nenhuma sobre ordem.

caracteres 1 2  +  3 4 Lexer análise léxica tokens INT(12) PLUS INT(34) EOF o EOF marca o fim da fita
O Roteiro 2 formaliza isso com uma classe Token (type e value) e uma classe Lexer (source, position, next e select_next()).
4

Segunda responsabilidade: o Parser

O Parser não vê caracteres. Ele recebe a sequência de tokens e faz uma única pergunta: essa ordem obedece à gramática?

válidasegue o trilho até a saída
INT PLUS INT MINUS INT EOF
inválidanão há trilho que comece assim
PLUS INT EOF

Acompanhe o Parser percorrendo o diagrama. Troque o exemplo para ver a cadeia que sobra tokens:

Entrada
o Parser esperaencontrao que faz
—
5

O detalhe do run(): conferir o EOF

O run() recebe o código, cria o Lexer, posiciona no primeiro token, chama parse_expression() e — este é o ponto — confere se o próximo token é EOF.

Sem essa checagem, 1+2 3 passaria

O Parser reconheceria 1+2 perfeitamente, chegaria à saída do diagrama e devolveria 3 — deixando o 3 final para trás, sem reclamar. A checagem de EOF é o que garante que toda a entrada foi consumida, e não apenas um pedaço válido dela. Selecione o exemplo 1+2 3 acima para ver onde ele morre.

def run(code):
    Parser.lexer = Lexer(code)
    Parser.lexer.select_next()           # posiciona no primeiro token
    result = Parser.parse_expression()

    if Parser.lexer.next.type != "EOF":   # ← sem isto, "1+2 3" passaria
        raise Exception(...)

    return result
6

O fluxo completo

Gramática
↓
Diagrama sintático — a gramática com formato de código
↓
Lexer → transforma caracteres em tokens
↓
Parser → consome os tokens
↓
Parser valida a estrutura e confere o EOF

A partir daqui a disciplina deixa de ser sobre gramáticas no papel. A Aula 6 pega esse mesmo parser e o faz respeitar precedência; a Aula 8 o faz construir uma árvore em vez de calcular.

Próxima seçãoAula 5 · Linguagens regulares→
05 fecha o bloco de linguagens regulares

Expressões regulares, AFD/AFN, pumping e fechamento

O que esta aula fecha

Ela amarra tudo que se pode dizer sobre linguagens regulares — três formas equivalentes de descrevê-las, uma ferramenta para provar que algo não é regular, e as operações que preservam a classe. É a última aula antes de a linguagem do compilador precisar de mais poder.

1

O exemplo que motiva: validar e-mail

A aula parte de um problema concreto: descrever o formato de um endereço de e-mail. É o caso perfeito porque um e-mail tem estrutura, mas não tem aninhamento — e é exatamente essa a fronteira das linguagens regulares.

2

Três formas de dizer a mesma coisa

Uma linguagem regular pode ser descrita de três jeitos, e os três têm exatamente o mesmo poder. Para L = { abn : n ≥ 0 }:

Gramática lineargera a cadeia
S → a B
B → b B  |  ε
Autômato finitoreconhece a cadeia
q0 a q1 b
Expressão regulardescreve o padrão
a b*

Os três operadores das expressões regulares:

operadornomesignifica
a | bunião"a ou b"
a bconcatenação"a seguido de b"
a *estrela de Kleene"zero ou mais repetições de a"
3

AFD e AFN têm o mesmo poder

AFD — determinísticoum caminho só

Para cada par (estado, símbolo) existe uma única transição. Dado o estado atual e o caractere lido, não há escolha a fazer.

É o que o seu select_next() é.

AFN — não determinísticovários caminhos

Pode haver várias transições possíveis para o mesmo par. A palavra é aceita se algum caminho aceitar.

Converter uma gramática linear direto para autômato costuma produzir um AFN.

O AFN não é mais poderoso — só mais confortável

Esse é o ponto contraintuitivo da aula: AFD e AFN reconhecem exatamente as mesmas linguagens, as regulares. Todo AFN pode ser convertido num AFD equivalente (ao custo de possivelmente muitos mais estados). O não determinismo é uma conveniência de projeto, não um ganho de expressividade.

4

Pumping lemma: provando que algo NÃO é regular

A intuição em uma frase: se uma linguagem regular tem palavras arbitrariamente longas, o autômato que a reconhece precisa repetir algum estado — ele tem um número finito de estados e a palavra não tem limite. Todo estado repetido é um ciclo, e ciclo pode ser percorrido quantas vezes você quiser.

L regular  ⇒  ∃ p  tal que toda w ∈ L com |w| ≥ p se escreve  w = x y z  com:

    |xy| ≤ p        (o ciclo acontece cedo)
    |y| > 0          (o ciclo consome pelo menos um símbolo)
    x yi z ∈ L       para todo i ≥ 0

O uso é sempre por contradição. Para L = { anbn }: suponha regular, pegue o p garantido, escolha w = apbp. Como |xy| ≤ p, o trecho y só contém a. Bombeando com i = 2 você ganha a sem ganhar b — a igualdade quebra. Contradição.

w = xyz — a divisão obrigatória pelo lema
x yi z — a palavra bombeada
5

Fechamento: operações que preservam a classe

Dizer que uma classe é fechada sob uma operação significa: se você começa com linguagens da classe e aplica a operação, o resultado continua na classe. As regulares são generosas nesse quesito — fecham sob praticamente tudo.

Isso não é curiosidade: é ferramenta de prova. Se L ∩ R não for regular e R for regular, então L não podia ser regular — senão a interseção seria. O detalhamento completo, comparando com as livres de contexto, está na Parte II, aba Fechamento.

6

Questões decidíveis

Para linguagens regulares, várias perguntas têm resposta algorítmica garantida — o que, como a Aula 9 vai mostrar, deixa de valer nas classes mais acima:

perguntadecidível para regulares?
a palavra w pertence a L?sim
L é vazia?sim
L é finita ou infinita?sim
L1 = L2?sim
L1 ⊆ L2?sim
L = Σ*?sim
Gramática linear ↔ Autômato finito ↔ Expressão regular
↓
AFD ↔ AFN — mesmo poder expressivo
↓
Pumping lemma → ferramenta para provar NÃO regularidade
↓
Fechamento → operações que preservam regularidade
Próxima seçãoAula 6 · Precedência→
06 materializa-se no Roteiro 3

Precedência: EXPRESSION, TERM, FACTOR

Se você guardar só uma ideia da aula, guarde esta

A precedência não é colocada na marra. Ela aparece porque a gramática é organizada em níveis, e os níveis mais internos terminam antes dos externos. Não existe tabela de prioridades em lugar nenhum do código.

1

Um nível por tipo de construção

EXPRESSION = TERM,   { ("+" | "-"), TERM }   ;
TERM       = FACTOR, { ("*" | "/"), FACTOR } ;
FACTOR     = ("+" | "-"), FACTOR | "(", EXPRESSION, ")" | NUMBER ;
nívelcuida deliga
EXPRESSION+ e -menos forte
TERM* e /médio
FACTORnúmero, unário, parêntesesmais forte

A primeira regra lê-se assim: uma expressão começa com um TERM e pode ser seguida por zero ou mais blocos do tipo + TERM ou - TERM. A segunda faz o mesmo para multiplicação e divisão.

2

Por que 3+4*2 vira 3+(4*2)

Porque parse_expression() chama parse_term(), que chama parse_factor(). O TERM termina de resolver 4*2 antes de devolver esse resultado para o EXPRESSION. Acompanhe o vaivém:

E                      # parse_expression começa
↓
  T                    # chama parse_term
  ↓
    F  lê 3            # parse_factor consome o 3
  ↑
  T  vê + e termina    # + não é dele, devolve 3
↑
E  consome o +
↓
  novo T
  ↓
    F  lê 4
  ↑
  T  vê *              # * É dele, fica com ele
  ↓
    novo F  lê 2
  ↑
  T  termina 4 * 2 = 8 # resolve antes de devolver
↑
E  termina 3 + 8 = 11

Repare na linha "T vê + e termina": o + não pertence àquele nível, então o TERM devolve o que tem e deixa o + para quem o chamou. Esse deixar para o de cima é o mecanismo inteiro da precedência.

O depurador da aba v1.1, na Parte III, mostra esses quadros de chamada empilhando e desempilhando de verdade, cada um com o seu próprio result.

●

Os três diagramas, em movimento

Cada regra da gramática é um trilho. Você entra pela esquerda, segue os caminhos e sai pela direita — e um retângulo com nome de regra significa "vá percorrer aquele outro trilho e volte". Digite uma expressão e veja o percurso acontecer.

Expressão
EXPRESSIONpercorrendo
TERM + − o laço volta para ler outro TERM
TERMpercorrendo
FACTOR * / o laço volta para ler outro FACTOR
FACTORpercorrendo
number + − FACTOR ( EXPRESSION )
Token atual
Diagramas abertos
Resultado parcial
Que pedaço do texto cada diagrama cobre
FACTOR TERM EXPRESSION
passo —
—
O que observar enquanto roda

Com 3+4*2: o EXPRESSION mal começa e já manda para o TERM, que manda para o FACTOR, que finalmente consome o 3. Na volta, o TERM olha se há * ou / — não há — e devolve. Só então o EXPRESSION vê o +. Mais adiante, quando o TERM encontra o *, ele não devolve: fica ali resolvendo 4*2 antes de entregar 8 para a soma. É a precedência acontecendo.

Experimente (2+3)*4 e veja a coluna "trilhos abertos" crescer: dentro do parêntese há um EXPRESSION por dentro de outro. Essa pilha é a memória que um autômato finito não tem.

3

Operadores unários

Em -3, o - não é subtração entre dois operandos: ele se aplica diretamente ao 3. Por isso a gramática permite FACTOR → "-" FACTOR e FACTOR → "+" FACTOR.

E como FACTOR chama outro FACTOR, construções como --3, +-3 e +--++3 funcionam sem nenhum código específico para unários encadeados. É recursão fazendo o trabalho.

4

Parênteses — e o ciclo que se fecha

FACTOR → "(" EXPRESSION ")". Quando o parser encontra (, ele começa uma EXPRESSION nova lá dentro. O nível mais interno volta ao mais externo, e o ciclo se fecha:

(2 + 3) * 4

FACTOR
↓
  (
  ↓
    EXPRESSION        # de volta ao topo da gramática
    ↓
      2 + 3
  ↓
  )
↑
esse FACTOR inteiro vira o operando esquerdo do *
É aqui que a linguagem sobe de classe

Aninhamento arbitrário é justamente o que um autômato finito não consegue controlar. Esta aula introduz recursão na gramática; a Aula 7 explica por que isso exige uma máquina mais forte, com pilha.

5

O que o Lexer precisa ganhar

Quase nada — quatro tokens novos: MULT, DIV, OPEN_PAR, CLOSE_PAR, somados aos já existentes PLUS, MINUS, INT e EOF. Para a análise léxica, * e + são igualmente triviais: ela não sabe nem se importa que um amarra mais forte que o outro.

EXPRESSION — cuida de + e −
↓ chama
TERM — cuida de * e /
↓ chama
FACTOR — NUMBER | ±FACTOR | ( EXPRESSION )
●

Para onde a gramática vai

A partir do Roteiro 5 a linguagem deixa de ser uma expressão solta e vira um programa: várias linhas, variáveis e saída. Vale ver agora, porque quase tudo que você já entendeu continua igual — e o que muda, muda pouco.

PROGRAM
STMT
Um programa é zero ou mais comandos. Só isso — e é aqui que o laço mais externo do compilador vai morar.
STMT
IDEN = EXPR PRINT ( EXPR ) EOL
Um comando é uma atribuição (x = EXPR), um print, ou nada — repare na linha reta de cima, que pula os dois e vai direto ao EOL: é o que permite linha em branco. E todo comando termina em EOL, o token de fim de linha.
FACTOR — com uma alternativa a mais
INT IDEN + − FACTOR ( EXPR )
O quarto caminho é novo: além de INT, unário e parêntese, um fator agora pode ser um IDEN — o nome de uma variável. É a única mudança no FACTOR.
O que NÃO muda — e é a maior parte

EXPR e TERM são idênticos aos que você percorreu acima: mesmo laço, mesmos operadores, mesma precedência. A hierarquia de três níveis que você aprendeu na Aula 6 fica intacta enquanto a linguagem cresce por cima dela. Duas camadas novas aparecem acima (PROGRAM e STMT) e uma alternativa nova dentro do FACTOR.

o que apareceo que significaconsequência no código
PROGRAMzero ou mais comandosum while no topo, até o fim do arquivo
STMTatribuição, print, ou linha vazianovo nível de parse_statement()
IDENnome de variávelo Lexer passa a reconhecer letras; nó Identifier
EOLfim de linha, um token de verdadeo \n deixa de ser espaço em branco descartável
=atribuição, não comparaçãonó Assignment com dois filhos
PRINTpalavra reservadao Lexer separa identificadores de palavras-chave
E o que isso exige que a gramática sozinha não dá

Com variáveis entra uma pergunta que nenhum diagrama responde: essa variável foi declarada antes de ser usada? Sintaticamente y = x + 1 é impecável mesmo que x nunca tenha existido. É a fronteira formal que a hierarquia de Chomsky descreve — e por isso entra uma tabela de símbolos, passada como argumento para o evaluate(), numa fase semântica separada.

Próxima seçãoAula 7 · GLC e autômatos de pilha→
07 a linguagem sobe de classe

Gramáticas livres de contexto e autômatos de pilha

A motivação vem direto da Aula 6

Assim que o compilador passou a aceitar parênteses aninhados — (a), ((a)), (((a))) — a linguagem deixou de ser regular. Um autômato finito não tem como controlar quantos parênteses foram abertos e ainda precisam fechar.

1

Gramática livre de contexto

A definição formal continua a mesma quádrupla:

G = ( V , Σ , P , A )

    V   vocabulário           P   produções
    Σ   terminais             A   símbolo inicial

A novidade está na forma das regras:

α → β        com  α ∈ N  (um único NÃO TERMINAL)
                    β ∈ V*  (qualquer combinação de terminais e não terminais)

Em português: o lado esquerdo é sempre um não terminal sozinho — daí "livre de contexto": não importa o que está em volta, a regra pode ser aplicada. O lado direito, porém, é livre.

A gramática do aninhamento:

A → ( A )
A → a

gera:  a  ·  (a)  ·  ((a))  ·  (((a)))  …

Ela não é regular, porque A → (A) tem um terminal de cada lado do não terminal. Uma gramática linear só admite o não terminal numa ponta.

2

Por que um AF não basta

A aula tenta montar um autômato finito para essa linguagem. O resultado aceita os casos certos — e também aceita lixo:

deveria aceitare aceita
a
(a)
((a))
deveria rejeitarmas aceita também
a)
((a)
(a))

O diagnóstico: o autômato não tem memória para casar aberturas com fechamentos numa quantidade arbitrária. Ele sabe que leu um parêntese; não sabe quantos.

3

O autômato de pilha

Autômato finitosem memória
estado + símbolo atual
Autômato de pilhamemória ilimitada, mas só no topo
estado + símbolo atual + pilha

A regra para parênteses é de uma simplicidade desconcertante: lê ( → empilha; lê ) → desempilha. A pilha guarda exatamente quantas aberturas estão pendentes.

Rode o autômato. Se ao final a entrada acabou e a pilha está vazia, a palavra é aceita:

Fita de entrada
Ação
Pilha
—
4

Aceitação por pilha vazia

O PDA que a aula constrói usa um critério de aceitação diferente do habitual: não há estado final. A palavra é aceita quando a entrada terminou e a pilha está vazia. Isso parece um detalhe técnico, mas é o que torna a equivalência do próximo passo elegante de provar.

5

O resultado central: GLC ↔ PDA

O teorema vale nos dois sentidos:

GLC → PDA

Dada uma gramática livre de contexto G, dá para construir um autômato de pilha não determinístico com aceitação por pilha vazia que reconhece exatamente L(G).

PDA → GLC

E o inverso: dado um PDA com aceitação por pilha vazia, dá para construir uma GLC equivalente.

É o mesmo tipo de correspondência que você já tinha visto um nível abaixo:

classegramáticareconhecedor
Regulargramática linearautômato finito
Livre de contextoGLCautômato de pilha
Cuidado: a equivalência é com PDA NÃO determinístico

Nem toda linguagem livre de contexto pode ser reconhecida por um PDA determinístico. Então não vale pensar "CFL = PDA determinístico". As linguagens que têm PDA determinístico formam uma subclasse própria — e não é coincidência que seja mais ou menos onde vivem as linguagens de programação, que precisam ser analisadas rápido e sem ambiguidade. A Aula 9 volta a esse ponto com LR(k).

6

Onde isso aparece na prática

Toda estrutura aninhada é livre de contexto e não regular. Por isso nenhuma das três abaixo pode ser validada corretamente só com expressão regular:

Linguagens de programação

Blocos, parênteses, chamadas aninhadas.

HTML / XML

Tags que abrem e fecham em qualquer profundidade.

JSON

Objetos dentro de arrays dentro de objetos.

parênteses / aninhamento
↓
AF não consegue lembrar arbitrariamente
↓
Gramáticas livres de contexto
↓
Autômatos de pilha — a pilha fornece memória
↓
GLC ↔ PDA
Próxima seçãoAula 8 · Árvores e ambiguidade→
08 prepara o Roteiro 4

Árvores de derivação, ambiguidade e AST

A frase que resume a aula inteira

Na Aula 6, o Parser já sabia calcular a expressão enquanto lia. Na Aula 8, ele passa a construir uma árvore que representa a expressão — e quem calcula é a árvore, depois. Todo o resto da aula existe para justificar essa troca.

1

Primeiro: o que é uma derivação

Uma derivação é o passo a passo de como a gramática gera uma cadeia. Você começa no símbolo inicial e vai trocando não-terminais pelas suas regras, até sobrar só texto. Para ver a ideia limpa, a aula usa a menor gramática interessante que existe:

S → a S b  |  ε        # ε (épsilon) = a palavra vazia

Ela gera ab, aabb, aaabbb… sempre a mesma quantidade de a e de b — o mesmo padrão dos parênteses da Aula 7, e pelo mesmo motivo não é regular.

Avance os passos e veja a derivação e a árvore crescerem juntas. A árvore é só a derivação desenhada: cada substituição vira um nó com seus filhos.

Derivação
—
2

O problema: ambiguidade

Uma gramática é ambígua quando uma mesma cadeia admite duas árvores de derivação diferentes. Parece detalhe teórico. Não é: significa que o mesmo programa pode ter dois significados. Considere a gramática ingênua que qualquer um escreveria primeiro:

E → E "+" E  |  E "*" E  |  number

Ela aceita 3+4*2. O problema é como — de duas formas, ambas válidas segundo as regras:

Árvore 1 → 3+(4*2)o * agarra primeiro
= 11
Árvore 2 → (3+4)*2o + agarra primeiro
= 14
Mesma entrada, duas respostas

11 ou 14. Nada na gramática decide qual. Um compilador com essa gramática produziria resultados dependentes de um detalhe de implementação — qual regra o parser tenta primeiro. A linguagem deixaria de ter significado definido.

3

A solução: estratificar a gramática

É aqui que EXPRESSION, TERM e FACTOR deixam de parecer burocracia. Eles não foram inventados para organizar o código — foram inventados para eliminar a ambiguidade. Um TERM nunca pode conter um + solto; para isso teria que descer até FACTOR e entrar num parêntese. Então a Árvore 2 acima simplesmente não é derivável. Sobra uma, e só uma.

gramáticaárvores para 3+4*2precedência
E → E+E | E*E | nduas — ambíguaindefinida
EXPRESSION / TERM / FACTORuma sóembutida na hierarquia
4

Um caso clássico: o dangling else

Ambiguidade não é problema só de contas. O exemplo mais famoso em linguagens de programação é o else pendurado:

if (a == b)
    if (c <= n)
        d++;
else
    d--;

Esse else pertence a qual if? A indentação sugere o primeiro, mas indentação não é gramática:

Leitura A — o else é do if de DENTRO
if (a == b) {
    if (c <= n)
        d++;
    else
        d--;
}
Leitura B — o else é do if de FORA
if (a == b) {
    if (c <= n)
        d++;
}
else
    d--;

Com a == b verdadeiro e c <= n falso, a Leitura A executa d-- e a Leitura B não executa nada. Comportamentos diferentes, mesmo texto.

Como as linguagens reais resolvem

C, Java, C# e companhia adotam a regra "o else pertence ao if mais próximo ainda sem else" — ou seja, a Leitura A. É uma decisão imposta por fora da gramática, que continua ambígua no papel. Python resolve de raiz: a indentação é sintaxe, então a pergunta nem chega a existir.

5

Da árvore de derivação para a AST

Resolvida a ambiguidade, sobra uma árvore — mas ela é grande demais. Para chegar ao número 3 em 3+4*2, a árvore de derivação desce por EXPRESSION → TERM → FACTOR → NUMBER, mesmo não havendo multiplicação nenhuma naquele ramo.

Esses nós cumpriram o papel deles — provar que a cadeia obedece à gramática — e depois disso não servem para mais nada. A AST é o que sobra quando você joga fora o andaime. Por isso ela é abstrata: o operador, que na derivação era um terminal consumido de passagem, vira o nó; e os não-terminais, que eram nós, desaparecem.

Na Parte II, aba Árvores, há um gerador que desenha as duas lado a lado para a expressão que você digitar.

6

Como a AST é executada

A avaliação é recursiva: cada nó resolve a própria operação, mas só depois de pedir aos filhos que resolvam a parte deles. Acompanhe a conversa para 3+4*2:

Três coisas para guardar dessa conversa. A execução vai de baixo para cima — a raiz é a última a produzir um valor, e é o valor final. Nenhum nó sabe quem é o seu pai. E o + não faz ideia de que o filho direito é uma multiplicação: ele pede um número e recebe um número. É exatamente isso que o depurador da aba v1.2 anima nó a nó.

7

A ponte para o Roteiro 4

quemfaz o quêno código
Parserentende a estruturaparse_expression / term / factor
ASTrepresenta a estruturaIntVal · UnOp · BinOp
evaluate()executa a estruturaum por classe de nó
Gramática
↓
Derivação → árvore de derivação
↓
Problema: ambiguidade
↓
Corrigir a gramática (EXPRESSION / TERM / FACTOR)
↓
Parser constrói a AST
↓
AST é avaliada recursivamente
Próxima seçãoAula 9 · LL, LR e limites→
09 estratégias de parsing e limites formais

LL, LR, e até onde as linguagens vão

A aula tem duas metades que não parecem conversar — mas conversam

A primeira é prática: LL(k) e LR(k) são duas estratégias para escrever um parser. A segunda é teórica: o pumping lemma para CFL, as propriedades de fechamento e a hierarquia de Chomsky dizem até onde cada classe alcança. O fio que liga as duas: as estratégias de parsing existem porque certas classes de gramática são tratáveis e outras não.

1

LL(k): analisar de cima para baixo

Um parser LL parte do símbolo inicial e vai expandindo regras até chegar na cadeia. As três letras significam:

siglasignificana prática
LLeft to rightos tokens são lidos da esquerda para a direita
LLeftmost derivationexpande sempre o não-terminal mais à esquerda
klookaheadquantos tokens precisa espiar para escolher a produção

O exemplo do material:

A → a A b  |  c              L = { am c bm : m ≥ 0 }

Essa gramática é LL(1): olhando um único token, a decisão já está tomada. Se o próximo for a, use A → aAb. Se for c, use A → c. Nunca há dúvida.

O seu compilador é um parser LL(1) — escrito à mão

Olhe o parse_factor(): ele consulta Parser.lexer.next.type — exatamente um token — e decide entre três alternativas (número, unário, parêntese). Isso é literalmente a definição de LL(1). O nome técnico do que você escreveu é descida recursiva (recursive descent): uma função por não-terminal, um token de lookahead. E é por isso que o atributo se chama next — ele é o lookahead.

def parse_factor():
    token = Parser.lexer.next        # k = 1: um token, e só

    if token.type == "INT":        # escolhe a produção
        ...
    if token.type in ("PLUS", "MINUS"):
        ...
    if token.type == "OPEN_PAR":
        ...
2

LR(k): analisar de baixo para cima

Um parser LR faz o caminho inverso: parte da cadeia e vai reduzindo pedaços até sobrar o símbolo inicial. Ele trabalha com uma pilha e duas operações:

shiftempilhar

Tira o próximo token da entrada e empurra na pilha. É o "ainda não sei o que isso vai virar, guarda aí".

reducereconhecer e trocar

O topo da pilha casa com o lado direito de uma produção. Troca pelo lado esquerdo. É o "ah, isso aqui era um TERM".

Acompanhe um shift-reduce completo. A gramática é E → E+T | T e T → n, e a entrada é n+n:

pilhaentradaação
—

Repare no formato: a pilha cresce com os shifts e encolhe com os reduces, e no fim resta exatamente E. Foi o caminho oposto ao da árvore de derivação da Aula 8, que ia de S para a cadeia.

3

LL contra LR, lado a lado

LL(k) · top-down expande a gramática até bater com a entrada EXPRESSION expande TERM + TERM n + n a entrada LR(k) · bottom-up reduz a entrada até chegar na gramática E reduz E + T n + n a entrada
As setas apontam em sentidos opostos: é a diferença inteira entre as duas famílias.
LL(k)LR(k)
direçãotop-down — do símbolo inicial para a cadeiabottom-up — da cadeia para o símbolo inicial
mecanismoescolher a produção e expandirshift / reduce sobre uma pilha
como se escreveà mão, por descida recursivaquase sempre gerado por ferramenta (yacc, bison, ANTLR)
podermenos gramáticas aceitasmais gramáticas aceitas
legibilidadeo código parece a gramáticatabelas de estados ilegíveis para humanos
o seu compiladoré este — LL(1)—
4

As subclasses de gramáticas livres de contexto

Nem toda gramática livre de contexto é tratável do mesmo jeito. Elas se encaixam umas nas outras:

todas as gramáticas livres de contexto não ambíguas LR(k) — determinísticas LL(k) descida recursiva — onde o seu compilador vive cada caixa está contida na de fora
Ganhar poder de parsing custa simplicidade: quanto mais para fora, mais gramáticas aceitas e menos legível o parser.
Não confunda: ambiguidade da gramática ≠ não determinismo do autômato

São coisas separadas, e o material avisa explicitamente. Ambiguidade é propriedade da gramática: existe cadeia com duas árvores de derivação. Não determinismo é propriedade do autômato: em algum ponto ele teria mais de uma transição possível. Uma gramática não ambígua pode exigir um PDA não determinístico. Uma fala sobre o que a linguagem significa; a outra, sobre como a máquina caminha.

5

Os limites: pumping, fechamento e Chomsky

A segunda metade da aula troca "como analisar" por "o que é possível analisar". Os três tópicos estão desenvolvidos em detalhe na Parte II, cada um na sua aba:

LL(k) → parsing top-down
↓
LR(k) → parsing bottom-up, shift/reduce
↓
subclasses: LL ⊂ LR ⊂ não ambíguas ⊂ CFG
↓
Pumping para CFL → z = uvwxy, bombeia v e x juntos
↓
Fechamento → quais operações preservam a classe
↓
Hierarquia de Chomsky → Regular ⊂ CFL ⊂ CSL ⊂ RE
Próxima seçãoParte II · A teoria por trás→
Parte II

A teoria por trás

Quatro ideias que explicam por que o compilador tem duas fases, por que o FACTOR precisa ser recursivo, e por que parênteses não viram nó na árvore. Não são pré-requisitos abstratos: cada uma resolve um problema concreto do seu código.

A gramática da linguagem

Uma gramática define quais sequências de símbolos são válidas. A do seu compilador tem três regras, e cada uma vira uma função — quase palavra por palavra.

EXPRESSION = TERM, { ("+" | "-"), TERM } ;
TERM       = FACTOR, { ("*" | "/"), FACTOR } ;
FACTOR     = NUMBER | ("+" | "-"), FACTOR | "(", EXPRESSION, ")" ;
NUMBER     = DIGIT, { DIGIT } ;
DIGIT      = "0" | "1" | ... | "9" ;
conceitoo que éaqui
Terminalsímbolo que aparece na string final; não se expande mais"0"…"9", "+", "-", "*", "/", "(", ")"
Não-terminalnome de uma regra; sempre se expandeEXPRESSION, TERM, FACTOR, NUMBER, DIGIT
Símbolo inicialpor onde toda derivação começaEXPRESSION
Produçãouma regra, do = ao ;as cinco acima
Linguagemo conjunto de todas as strings deriváveisinfinito

O dicionário EBNF → código

EBNFsignificavira
,seguido deuma linha após a outra
{ ... }repita zero ou mais vezeswhile
( A | B )A ou Bif / elif
[ ... ]opcionalif
"x"símbolo literalconsumir um token
NOMEoutra regrachamada de função

Derivação: gerando uma string a partir da gramática

Uma string pertence à linguagem se existir um caminho do símbolo inicial até ela. Cada passo substitui um não-terminal pela sua regra. Compare as duas expressões — repare onde o 3*4 aparece agrupado:

Exemplo
—
Por que "1 1" não pertence à linguagem

Tente derivar. Depois de EXPRESSION ⇒ TERM … você consome o primeiro 1. Para chegar ao segundo, precisaria de uma regra que coloque dois TERM lado a lado sem operador entre eles — e ela não existe. Sem derivação, a string está fora da linguagem. A rejeição não é uma regra ad-hoc que alguém inventou no código: é consequência da gramática.

BNF, EBNF, e o que a recursão à esquerda revela

A mesma linguagem, escrita em BNF pura — sem as chaves de repetição:

<expression> ::= <term>
               | <expression> "+" <term>
               | <expression> "-" <term>

Onde a EBNF usa { }, a BNF precisa de recursão. E repare onde: <expression> aparece à esquerda do operador. Isso se chama recursão à esquerda, e não é detalhe de escrita — ela codifica a associatividade:

10-3-2agrupa comoresultadocorresponde a
recursão à esquerda(10-3)-25✓ o while do seu código
recursão à direita10-(3-2)9errado para subtração

O while que acumula da esquerda para a direita no parse_expression() é a tradução direta dessa recursão à esquerda. Por isso 10-3-2 dá 5 no seu código sem você ter pensado no assunto.

Próxima seçãoAutômatos finitos→

O Lexer é um autômato finito

Um autômato finito determinístico (AFD) é a máquina mais simples que existe para reconhecer padrões: um conjunto de estados, uma transição por símbolo lido, e alguns estados marcados como de aceitação. Ele tem uma limitação definidora — nenhuma memória além do estado atual. Não conta, não empilha, não lembra o que veio antes.

E é exatamente isso que o seu select_next() é. A variável position não é memória do autômato: é só o ponteiro de leitura da fita.

início q0 início espaço dígito q1 NÚMERO dígito + − * / ( ) q2 SÍMBOLO qualquer outro erro Círculo duplo = estado de aceitação: chegou aqui, um token foi reconhecido. O laço em q1 é o que faz "22" virar UM token: ele fica no mesmo estado enquanto houver dígito, sem contar quantos. Note que NÃO existe estado para "dentro de parênteses". Um autômato finito não consegue guardar quantos abriram — e é disso que trata a aba seguinte.
O autômato do select_next(). Cada chamada parte de q0 e para ao chegar num estado de aceitação.

Rode o autômato

Digite algo e avance caractere a caractere. Observe que o estado nunca guarda mais do que "estou lendo um número" ou "não estou".

Fita de entrada
—
Estado
q0
Tokens reconhecidos
Transição
—
Linguagem regular = reconhecível por autômato finito

A camada léxica da sua linguagem — o que é um número, o que é um símbolo — é regular. É por isso que o Lexer quase não mudou do Roteiro 1 ao 4: acrescentar *, /, ( e ) foi só acrescentar transições saindo de q0. A estrutura da máquina continuou a mesma.

Próxima seçãoPumping lemma→

O lema do bombeamento

O lema do bombeamento (pumping lemma) é uma ferramenta para provar que uma linguagem NÃO é regular — ou seja, que nenhum autômato finito consegue reconhecê-la, por mais estados que você lhe dê.

Ele importa muito concretamente aqui: é a razão formal de o FACTOR precisar chamar a si mesmo.

O enunciado

Se L é regular, existe um número p (o comprimento de bombeamento) tal que toda palavra w ∈ L com |w| ≥ p pode ser escrita como w = xyz, satisfazendo:
  (1) |xy| ≤ p   (2) |y| ≥ 1   (3) xyiz ∈ L para todo i ≥ 0

A intuição é simples: um autômato com p estados que lê uma palavra de p ou mais símbolos precisa repetir algum estado — não há estados suficientes para todos serem distintos. O trecho lido entre as duas visitas ao mesmo estado forma um laço. E um laço pode ser percorrido quantas vezes você quiser: zero, uma, mil. Se a linguagem é regular, todas essas repetições continuam válidas.

Então, para provar que algo não é regular, basta achar uma palavra que quebra ao ser bombeada.

Aplicando: parênteses balanceados não são regulares

Considere L = { (n )n : n ≥ 1 } — a linguagem dos parênteses que abrem e fecham na mesma quantidade. Suponha, por absurdo, que seja regular. Então existe p, e tomamos w = (p)p.

Pela condição (1), xy cabe nos primeiros p símbolos — que são todos abre-parênteses. Logo y só contém (. Bombeando com i = 2, a palavra ganha parênteses abertos a mais e nenhum fechado. O balanço quebra. Contradição — a linguagem não é regular.

Mexa nos controles e veja a contradição acontecer:

w = xyz (a divisão obrigatória pelo lema)
xyiz (a palavra bombeada)

x · y · z — o y só pode conter abre-parênteses, porque pela condição (1) ele tem que caber nos primeiros p símbolos.

A consequência direta no seu código

Se parênteses balanceados não são regulares, nenhum autômato finito os reconhece — portanto o Lexer jamais poderia ser encarregado disso. É preciso uma máquina mais forte: um autômato de pilha, que empilha ao abrir e desempilha ao fechar. E é exatamente isso que o Parser é. Ele nunca declara uma pilha — ele usa a pilha de chamadas do próprio Python, via a recursão do parse_factor(). Cada parêntese aberto é um quadro de chamada a mais.

classe de linguagemmáquina que reconhecememóriana sua linguagem
Regularautômato finitonenhumaa camada léxica: NUMBER, símbolos
Livre de contextoautômato de pilhauma pilhaa camada sintática, a partir dos parênteses

Até o Roteiro 2 a linguagem inteira era regular — sem parênteses, nada se aninhava, e em tese um único autômato finito daria conta. O Roteiro 3 é o momento em que ela sobe de classe.


A versão para linguagens livres de contexto

O mesmo raciocínio sobe um andar. Se uma CFL tem palavras arbitrariamente longas, a árvore de derivação de uma palavra suficientemente comprida precisa ser alta — e numa árvore alta, algum não-terminal reaparece no mesmo caminho, porque a gramática tem um número finito deles.

S A A u v w x y repetir o trecho entre os dois A gera mais v E mais x, ao mesmo tempo
O A de cima deriva v A x. Como o A de baixo é o mesmo não-terminal, esse pedaço pode ser repetido quantas vezes se queira — e cada repetição acrescenta um v à esquerda e um x à direita.
L livre de contexto  ⇒  ∃ n  tal que toda z ∈ L com |z| ≥ n se escreve  z = u v w x y  com:

    v x ≠ ε          (pelo menos um dos dois trechos é não vazio)
    |v w x| ≤ n       (os dois trechos ficam perto um do outro)
    u vi w xi y ∈ L    para todo i ≥ 0
A diferença que importa: duas partes bombeadas, não uma

No caso regular, a palavra quebra em três partes e você repete uma: w = xyz, bombeia o y. Nas CFL, quebra em cinco e você repete duas ao mesmo tempo: z = uvwxy, bombeia v e x juntos. E isso não é uma escolha: é consequência de a recursão numa árvore crescer para os dois lados do não-terminal repetido.

regulareslivres de contexto
decomposiçãow = x y zz = u v w x y
o que se repeteyv e x, juntos
de onde vemum ciclo no autômatoum não-terminal repetido na árvore
restrição de tamanho|xy| ≤ p|vwx| ≤ n
não vazio|y| > 0vx ≠ ε

O exemplo clássico: anbncn não é livre de contexto

A pilha de um PDA consegue casar duas quantidades — abre e fecha, a e b. Casar três ao mesmo tempo é demais para ela, e o pumping lemma prova isso.

Suponha CFL, pegue o n garantido e escolha z = anbncn. Como |vwx| ≤ n, o trecho vwx é curto demais para tocar as três letras — ele cobre no máximo duas delas (a e b, ou b e c). Bombeando com i = 2, as letras tocadas crescem e a terceira fica para trás. As três quantidades deixam de ser iguais. Contradição.

Por que isso importa para um compilador

anbncn parece artificial, mas tem análogo real: "toda variável usada foi declarada antes, com o tipo certo" é uma restrição desse tipo. É por isso que nenhuma gramática livre de contexto valida um programa por completo, e por que existe uma fase semântica separada — com tabela de símbolos — depois da sintática. A sintaxe cuida da forma; ela literalmente não alcança o resto.

Próxima seçãoPropriedades de fechamento→

Propriedades de fechamento

Dizer que uma classe de linguagens é fechada sob uma operação significa uma coisa só: se você pega linguagens daquela classe e aplica a operação, o resultado continua na classe.

L₁ , L₂ regulares   ⇒   L₁ ∪ L₂  é regular     ✓ fechada sob união
L₁ , L₂ livres de contexto   ⇒   L₁ ∩ L₂  pode NÃO ser livre de contexto

Isso parece contabilidade abstrata, mas é ferramenta de prova — e é assim que cai em avaliação.

A tabela que você precisa saber

operaçãoregulareslivres de contextoobservação
União  L₁ ∪ L₂simsimbasta um símbolo inicial novo com duas produções
Concatenação  L₁ L₂simsimS → S₁ S₂
Estrela de Kleene  L*simsimS → S₁ S | ε
Reversão  Lᴴsimsiminverte o lado direito de cada produção
Interseção  L₁ ∩ L₂simNÃOcontraexemplo abaixo
Complemento  L̄simNÃOdecorre da interseção, por De Morgan
Diferença  L₁ − L₂simNÃOL₁ ∩ L₂̄
Interseção com regular  L ∩ RsimSIMa exceção que salva as provas
Homomorfismosimsimtroca sistemática de símbolos
O contraexemplo que prova que CFL não fecha sob interseção

Tome estas duas, ambas livres de contexto:

L₁ = { an bn cm }
L₂ = { am bn cn }

Cada uma casa um par de quantidades e deixa a terceira livre — uma pilha dá conta disso. Mas a interseção exige que os três casem ao mesmo tempo:

L₁ ∩ L₂ = { an bn cn }

E essa, pelo pumping lemma para CFL (aba anterior), não é livre de contexto. Duas CFL cuja interseção não é CFL: a classe não é fechada sob interseção.

Por que "interseção com regular" ser fechada é tão útil

É o truque padrão nas provas. Para mostrar que uma linguagem L não é livre de contexto, você a intersecta com uma linguagem regular escolhida a dedo, obtendo algo reconhecidamente não-CFL. Como CFL é fechada sob interseção com regular, se L fosse CFL o resultado também seria — contradição. A regular funciona como um filtro que isola a parte difícil.

Como usar isso numa prova

# Para provar que L NÃO é regular usando fechamento:

1. suponha que L é regular
2. escolha uma linguagem R comprovadamente regular
3. como regulares fecham sob ∩, então L ∩ R seria regular
4. mostre (por pumping) que L ∩ R NÃO é regular
5. contradição ⇒ L não era regular

A vantagem sobre aplicar pumping direto em L: às vezes L é bagunçada demais para escolher uma palavra boa, mas L ∩ R fica limpinha.

Próxima seçãoHierarquia de Chomsky→

A hierarquia de Chomsky

Quatro classes encaixadas, cada uma estritamente maior que a anterior. A posição de uma linguagem na hierarquia responde a uma pergunta prática: que tipo de máquina é preciso construir para reconhecê-la?

Tipo 0 · Recursivamente enumeráveis a classe mais ampla · sem restrição nas regras Tipo 1 · Sensíveis ao contexto |α| ≤ |β| · o lado esquerdo pode ter contexto Tipo 2 · Livres de contexto autômato de pilha · A → β · o Parser vive aqui Tipo 3 · Regulares autômato finito · A → aB | a · o Lexer vive aqui a classe mais restrita, e a mais rápida de reconhecer
Cada nível é estritamente mais expressivo que o de dentro — e estritamente mais caro de reconhecer.
tipolinguagemforma das regrasreconhecedorexemplo típico
3RegularA → aB  |  A → aautômato finitoa b*
2Livre de contextoA → βautômato de pilhaaⁿ bⁿ
1Sensível ao contextoα → β, com |α| ≤ |β|fora do escopoaⁿ bⁿ cⁿ
0Recursivamente enumerávelα → β, sem restriçãofora do escopo—
Repare no que muda de um nível para o outro: o lado ESQUERDO das regras

No tipo 3, o lado direito é muito restrito (um terminal, opcionalmente seguido de um não-terminal). No tipo 2, o lado direito se liberta, mas o esquerdo continua sendo um único não-terminal — daí "livre de contexto": a regra vale onde quer que o não-terminal apareça. No tipo 1, o lado esquerdo pode ter contexto em volta (aAb → aβb), e é daí que vem o nome "sensível ao contexto".

Onde o seu compilador está

faseclassemáquinano código
Análise léxicaregular (tipo 3)autômato finitoLexer.select_next()
Análise sintáticalivre de contexto (tipo 2)autômato de pilhaParser — a pilha é a de chamadas do Python
Análise semânticaalém do tipo 2precisa de tabela de símboloschega nos próximos roteiros

Essa última linha é a moral da história. "Toda variável usada foi declarada" é uma restrição de tipo 1 disfarçada — gramática livre de contexto nenhuma expressa isso. Por isso todo compilador tem uma fase semântica separada: não é preguiça de projeto, é uma fronteira formal.

Próxima seçãoÁrvores: derivação vs. AST→

Duas árvores diferentes

A palavra "árvore" é usada para duas coisas distintas, e confundi-las atrapalha. A árvore de derivação registra cada regra da gramática que foi aplicada. A AST guarda só o que é necessário para calcular. Compare as duas para a mesma expressão:

Árvore de derivação toda regra aplicada vira um nó
AST — o que o seu código constrói só operadores e valores

A árvore de derivação é alta e repetitiva: para chegar ao número 2, é preciso descer por EXPRESSION → TERM → FACTOR → NUMBER, mesmo que nenhuma multiplicação exista ali. Esses nós intermediários provam que a string obedece à gramática — e depois disso não servem para mais nada.

Por que parênteses somem na AST

Experimente (2+3)*4 acima. Na árvore de derivação os parênteses aparecem como terminais consumidos; na AST, não há vestígio deles. Não é perda de informação: parênteses existem porque a escrita é linear — num texto de uma dimensão você precisa de marcadores para dizer o que agrupa com o quê. A árvore é bidimensional, e o agrupamento já está na posição do nó. Compare as ASTs de (2+3)*4 e 2+3*4: os formatos são diferentes, e é o formato que determina a ordem da conta.

O evaluate() é um percurso em pós-ordem

Percorrer uma árvore tem nomes conforme a ordem em que se visita a raiz:

percursoordempara que serve
pré-ordemraiz, esquerda, direitacopiar a árvore, imprimir em notação prefixa
em-ordemesquerda, raiz, direitareconstruir a expressão original (com parênteses)
pós-ordemesquerda, direita, raizavaliar — é o que o seu evaluate() faz

Faz sentido que seja pós-ordem: um BinOp não tem como somar antes de saber quanto valem os dois filhos. A raiz é sempre a última a produzir um valor — e é o valor final.

def evaluate(self):
    left  = self.children[0].evaluate()   # 1. desce à esquerda
    right = self.children[1].evaluate()   # 2. desce à direita
    ...                                   # 3. só então opera — pós-ordem

No depurador da aba v1.2, mais abaixo, você vê esse percurso acontecendo: os nós acendem descendo até as folhas e os valores vão aparecendo de baixo para cima.

Próxima seçãoParte III · Os roteiros→
Parte III

Roteiro a roteiro

Cada aba traz o que mudou naquela versão, o código-fonte completo da tag correspondente, e um depurador que executa aquela versão específica do compilador — escreva a expressão que quiser e percorra passo a passo.

classev0.0v1.0v1.1v1.2
Tokennasce===
Tokenizer → Lexernascerenomeada+ SYMBOLS=
Parsernascevira estática+ term, factordevolve nós
Node———nasce
IntVal———nasce
UnOp———nasce
BinOp———nasce
v0.0 Roteiro 1 · soma e subtração de naturais

Nascem três classes

TokenTokenizerParser

O enunciado pedia apenas um main.py que recebesse '1+ 2 -3' e imprimisse 0 — nenhuma classe era exigida. Mesmo assim o código já nasceu dividido em três, porque o problema tem uma tensão que só a divisão resolve.

A tensão: o espaço tem dois papéis contraditórios

Em "789 +345" o espaço é lixo e deve sumir. Em "1 1" ele é exatamente o que torna a entrada inválida. Se você começar com codigo.replace(" ", ""), "1 1" vira "11" e o programa imprime onze em vez de dar erro. O espaço não pode ser apagado do texto — precisa ser consumido entre tokens, e isso só é possível varrendo caractere a caractere.

class Token 2 atributos · atravessa os 4 roteiros sem mudar
typea categoria gramatical — em v0.0 apenas INT, PLUS, MINUS, EOF
valueo conteúdo concreto: 22, "+", ""

Duas perguntas diferentes se fazem a um token, e cada uma usa só metade dele. Quem valida a ordem olha o type — "depois de um número cabe um operador" não depende de qual número. Quem calcula olha o value.

class Tokenizer 3 atributos · 1 método
sourcea string de entrada, guardada inteira e nunca modificada
positiono índice do próximo caractere — a memória do cursor
nexto último Token separado
select_next()lê o próximo token e o guarda em next

O trecho decisivo é o laço interno dos dígitos — o que faz "22", dois caracteres, virar um token. As duas condições do while são obrigatórias, nessa ordem: o and do Python só avalia o lado direito se o esquerdo for verdadeiro, então numa entrada terminada em dígito ele nunca lê uma posição inexistente.

class Parser com instâncias · vira estática em v1.0

Em v0.0 o Parser ainda tinha __init__ e métodos com self. Ele implementava EXPRESSION = NUMBER, { ("+" | "-"), NUMBER } em quatro passos: exigir um INT, inicializar o resultado, repetir enquanto houver operador, e conferir o EOF no fim.

Repare que o laço é sobre operadores, não sobre "ainda não acabou". É essa escolha que rejeita "1 1": depois de consumir o primeiro 1, o próximo token é INT, que não entra no while — e sobra para a checagem de EOF reprovar.

Depurador — gramática do Roteiro 1

Só + e -. Não há precedência nem parênteses: o resultado é acumulado da esquerda para a direita numa única variável.

Código completo · main.py na tag v0.0.0
— Extra credit

O Roteiro 1 não tem exercício extra

O enunciado diz explicitamente: "Não há extra credit para esse roteiro". Os exercícios extras começam na versão v1.0, e cada um usa uma tag com prefixo x — x1.0, x1.1, x1.2 — desenvolvida numa branch separada, criada a partir da tag aprovada do roteiro, e nunca mesclada na main.

Próxima seçãov1.0 · Separação de fases→
v1.0 Roteiro 2 · mesma linguagem, outra arquitetura

Separação de fases

Tokenizer → LexerParser estáticoprefixos de erro

Nenhuma classe nova, nenhuma funcionalidade nova. A gramática é idêntica à do Roteiro 1 — mesmos tokens, mesmos resultados, mesma bateria de testes. O que muda é só quem executa o quê, e por quê.

v0.0 — instância
class Parser:
    def __init__(self):
        self.tokenizer = None

    def parse_expression(self):
        ...

# uso:
print(Parser().run(sys.argv[1]))
v1.0 — estático
class Parser:
    lexer = None

    @staticmethod
    def parse_expression():
        ...

# uso:
print(Parser.run(sys.argv[1]))

Estático significa que o membro pertence à classe, não a um objeto. Faz sentido aqui porque não existe cenário em que dois Parser analisem coisas diferentes ao mesmo tempo: há um programa sendo compilado, e um só.

O conceito que este roteiro realmente introduz: o contrato do next

O atributo next guarda o token já lido mas ainda não consumido — o lookahead. Ele permite ao Parser perguntar sem consumir: if lexer.next.type == "PLUS" só olha; lexer.select_next() é que avança. O contrato: ao entrar em qualquer método do Parser, next já contém o próximo token não consumido; ao sair, o mesmo vale. Daí as duas regras que o enunciado do Roteiro 3 manda conferir — run() chama select_next() uma vez antes de tudo, e parse_expression() nunca chama na primeira linha.

class Lexer renomeada · agora sob demanda

Mudou o nome, mas o que mudou de verdade foi o modelo: em vez de o Parser receber uma lista pronta de tokens, ele puxa um por vez, quando precisa. Medi a diferença numa entrada de 800 mil caracteres com erro de sintaxe no caractere 4:

modelotempo até o errotokens em memória
lista completa antes de analisar1041 ms800 005
um token por vez0,1 ms5

São 13 mil vezes mais rápido — mas velocidade nem é o motivo principal. Em linguagens reais, o mesmo caractere vira tokens diferentes conforme o contexto: o < de a < b é comparação, o de #include <stdio.h> faz parte de um nome de arquivo. Só o Parser sabe qual é o caso — e não tem como informar nada se a lista já foi gerada inteira.

Os prefixos [Lexer] e [Parser]

Toda exceção passa a dizer qual fase reclamou, e isso já elimina metade do código da busca. "1$2" morre no Lexer — caractere que não existe na linguagem. "1 1" passa ileso pelo Lexer, que vê dois números e não tem opinião sobre ordem, e morre no Parser.

Depurador — gramática do Roteiro 2

A mesma gramática do Roteiro 1. O que mudou é interno: o token vem do Lexer sob demanda, e as chamadas são estáticas.

Código completo · main.py na tag v1.0.0
x1.0 Extra credit · tag x1.0.0

Operador XOR   ^

Enunciado: implementar o XOR entre inteiros. O símbolo é ^, e ele tem a mesma precedência de + e -, logo a operação é realizada por ordem de chegada. Exemplo: 2 ^ 3 → 1.

A pergunta que decide tudo: em que nível ele entra?

"Mesma precedência de + e -" responde sozinha: ele entra no mesmo laço em que já estão o + e o -. Não se cria nível novo, não se mexe na hierarquia. Este é o caso fácil — e é o contraste que faz os dois próximos extras serem entendidos.

EXPRESSION+   −   ^o ^ entra aqui, junto dos que já existiam

O que mudou no código

# 1. Lexer: mais um símbolo de um caractere
elif char == "^":
    self.position += 1
    self.next = Token("XOR", "^")

# 2. Parser: o XOR entra na MESMA condição do while
while Parser.lexer.next.type in ("PLUS", "MINUS", "XOR"):
    ...
    if operator == "PLUS":
        result += Parser.lexer.next.value
    elif operator == "MINUS":
        result -= Parser.lexer.next.value
    else:
        result ^= Parser.lexer.next.value

Duas mudanças, nenhuma estrutural. O laço já era "enquanto houver operador deste nível" — bastou ampliar a lista.

Casos verificados

entradasaídapor quê
2 ^ 3110 XOR 11 = 01
5^36101 XOR 011 = 110
1+2^30(1+2)^3 — ordem de chegada, não precedência
2^3+12(2^3)+1 = 1+1
12^10^60((12^10)^6) — associativo à esquerda
^3[Parser] Expected INT, found XORnão existe XOR unário

O caso 1+2^3 é o que prova a precedência: se o ^ ligasse mais forte, daria 1+(2^3) = 2. Dá 0, ou seja (1+2)^3 = 3^3. Está no nível certo.

Próxima seçãov1.1 · Precedência e recursão→
v1.1 Roteiro 3 · multiplicação, divisão, unários, parênteses

Precedência e recursão

Lexer.SYMBOLSparse_term()parse_factor()

A linguagem cresce de verdade pela primeira vez, e a resposta não é uma classe nova — são dois métodos.

Lexer.SYMBOLS quatro tokens novos
SYMBOLS = {
    "+": "PLUS",  "-": "MINUS",
    "*": "MULT",  "/": "DIV",
    "(": "OPEN_PAR",  ")": "CLOSE_PAR",
}

É só isso que a análise léxica sofre. Quatro entradas num dicionário — que, no autômato da aba Autômatos, são quatro transições novas saindo de q0. Precedência e aninhamento não são problema do léxico: para o Lexer, * e + são igualmente simples.

Parser · três níveis + parse_term · + parse_factor

Não existe, em lugar nenhum do código, uma tabela dizendo que * tem prioridade sobre +. A precedência é consequência de quem chama quem.

liga menos forte parse_expression() + − chama parse_term() * / chama parse_factor() INT · unário · ( ) ao achar ( volta ao topo é a pilha de chamadas do Python servindo de pilha do analisador mais externo mais interno quem termina primeiro amarra mais forte
O método mais fundo resolve o seu pedaço antes de devolver — por isso amarra mais forte.
Cada função tem a SUA variável local

Uma variável global quebraria 4/(1+1)*2: quando o parse_expression de dentro dos parênteses roda, já existe outro rodando por fora. No depurador abaixo, a coluna de quadros de chamada mostra isso acontecendo — cada quadro carrega o próprio result.

Depurador — gramática do Roteiro 3

Agora com os três níveis. Acompanhe a pilha de chamadas: ela cresce ao entrar em parse_term e parse_factor, e cada quadro tem o seu próprio resultado parcial.

Código completo · main.py na tag v1.1.1
x1.1 Extra credit · tag x1.1.0

Potência   **

Enunciado: implementar a potência entre inteiros. O símbolo é ** e o token se chama POWER. A precedência é maior que a dos unários, porém menor que a dos parênteses — e o próprio enunciado avisa: "logo será preciso quebrar o Factor em dois pedaços". Exemplo: -3 ** 2 → -9.

A técnica que provavelmente cai na prova: para inserir um nível, divide-se um nível

A precedência pedida fica entre duas coisas que hoje moram no mesmo método, o parse_factor(): o unário e o parêntese. Como a precedência é dada pela profundidade da chamada, não há como espremer algo entre elas sem separá-las em dois métodos. Daí o "quebrar o Factor em dois pedaços".

antes — um nível sóv1.1
FACTOR = NUMBER
       | ("+"|"-"), FACTOR
       | "(", EXPRESSION, ")" ;
depois — dois níveisx1.1
FACTOR = ("+"|"-"), FACTOR
       | BASE, { "**", BASE } ;

BASE   = NUMBER
       | "(", EXPRESSION, ")" ;

A escada de precedência fica assim — repare no nível novo espremido no meio:

EXPRESSION+   −liga menos forte
↓ chama
TERM*   /
↓ chama
FACTOR+ − unários
↓ chama
⋮ potência**nível NOVO — entre o unário e o parêntese
↓ chama
BASEnúmero   ( … )liga mais forte

O que mudou no código

# 1. Lexer: o ** precisa ser testado ANTES do *, senão vira dois MULT
if self.source.startswith("**", self.position):
    self.position += 2
    self.next = Token("POWER", "**")
elif char in Lexer.SYMBOLS:
    ...

# 2. Parser: FACTOR trata o unário e delega; o laço de ** opera sobre BASE
def parse_factor():
    if Parser.lexer.next.type == "PLUS":
        Parser.lexer.select_next()
        return Parser.parse_factor()
    if Parser.lexer.next.type == "MINUS":
        Parser.lexer.select_next()
        return -Parser.parse_factor()

    result = Parser.parse_base()
    while Parser.lexer.next.type == "POWER":
        Parser.lexer.select_next()
        result **= Parser.parse_base()
    return result

def parse_base():          # o pedaço que sobrou: só número e parêntese
    ...

Casos verificados

entradasaídapor quê
-3 ** 2-9a potência vem antes do unário: −(3**2)
(-3)**29o parêntese vem antes da potência
2*3**2182*(3**2) — potência antes do *
1+2**391+(2**3)
+-2**2-4unários encadeados sobre a potência
2**3**264(2**3)**2 — associativo à esquerda, ver ressalva
2 ** -3[Parser] ... found MINUSo expoente é BASE, não FACTOR
2***3[Parser] ... found MULTlê ** e sobra um *

As duas primeiras linhas são o par que prova a precedência. -3 ** 2 dá −9 porque a potência resolve antes do sinal; (-3)**2 dá 9 porque o parêntese resolve antes da potência. É exatamente o "maior que unário, menor que parêntese" do enunciado, verificável em dois testes.

Duas decisões que o enunciado não fixa — e podem ser perguntadas

Associatividade: 2**3**2 dá 64 aqui, porque o while acumula da esquerda para a direita, como os outros operadores. Matematicamente e em Python a potência é associativa à direita (daria 512). Para inverter, troca-se o while por recursão à direita: return result ** Parser.parse_factor().

Expoente negativo: 2 ** -3 é erro de sintaxe, porque o operando da direita é BASE e não FACTOR. É proposital: expoente negativo produziria fração e quebraria a garantia de resultado inteiro.

Próxima seçãov1.2 · A árvore sintática→
v1.2 Roteiro 4 · a árvore sintática abstrata

Quatro classes de nó

NodeIntValUnOpBinOpParser para de calcular

A mudança mais profunda dos quatro roteiros, e a que menos altera os resultados: a bateria de testes é exatamente a mesma do Roteiro 3. O que muda é quando a conta acontece.

v1.1 — o Parser calcula
while ... in ("PLUS", "MINUS"):
    ...
    if operator == "PLUS":
        result += Parser.parse_term()
    else:
        result -= Parser.parse_term()

# devolve um NÚMERO
v1.2 — o Parser monta
while ... in ("PLUS", "MINUS"):
    operator = Parser.lexer.next.value
    Parser.lexer.select_next()
    result = BinOp(operator,
                   [result, Parser.parse_term()])

# devolve um NÓ

O programa passa a ter dois tempos: montar (texto → árvore) e avaliar (árvore → número). O main() junta os dois em uma linha: print(Parser.run(code).evaluate()).

class Node(ABC) abstrata · o contrato
class Node(ABC):
    def __init__(self, value, children):
        self.value = value
        self.children = children

    @abstractmethod
    def evaluate(self):
        pass
valueo conteúdo do nó. Num BinOp é o operador; num IntVal, o próprio número
childrenuma lista de nós, não um par — a árvore não é binária
evaluate()abstrato: cada filha escreve o seu

Tentar Node("+", []) levanta TypeError. Um Node genérico não teria o que fazer no evaluate(), e se pudesse existir, o erro só apareceria muito depois, no meio de uma recursão, longe da causa.

Node (abstrata) value · children · evaluate() herda IntVal 0 filhos · folha return self.value UnOp 1 filho ± children[0] BinOp 2 filhos [0] op [1]
As três filhas diferem em uma coisa só: quantos filhos têm.
A regra de ouro: cada nó cuida só da própria vida

Um BinOp("+") não sabe se os filhos são números, contas gigantes ou — nos próximos roteiros — chamadas de função. Ele pede evaluate() e recebe um número. Nenhum nó olha o pai, nenhum olha os irmãos. É essa ignorância deliberada que permite acrescentar tipos de nó novos sem tocar nos existentes.

Depurador — gramática do Roteiro 4

Mesma gramática do Roteiro 3, mas agora você vê a árvore sendo montada nó a nó e, depois, o evaluate() descendo até as folhas e subindo com os valores.

Código completo · main.py na tag v1.2.1
x1.2 Extra credit · tag x1.2.0

Fatorial   !

Enunciado: implementar o fatorial de um inteiro. O símbolo é !, o token se chama FACT, e ele é posicionado após o valor. A precedência é maior que a dos unários e menor que a dos parênteses. Fatorial de negativo não é aceito. Exemplo: -3! → -6.

Mesma precedência do ** — logo, mesma técnica

"Maior que os unários, menor que os parênteses" é exatamente a posição da potência do extra anterior. Então a solução é a mesma quebra do FACTOR em dois. A única diferença é que o operador vem depois do operando (pós-fixo), e não entre dois operandos.

FACTOR = ("+"|"-"), FACTOR | BASE, { "!" } ;
BASE   = NUMBER | "(", EXPRESSION, ")" ;
Nenhuma classe de nó nova

O fatorial é uma operação unária — um operador, um operando. Então ele reusa o UnOp com value = "!". A árvore de -3! fica com dois UnOp empilhados:

UnOp('-')
   UnOp('!')
      IntVal(3)

Esse é o teste real de que a arquitetura do Roteiro 4 aguentou crescer: três operadores novos nos extras (^, **, !) e zero classes novas.

O que mudou no código

# 1. Lexer: uma entrada a mais na tabela
SYMBOLS = { ..., "!": "FACT" }

# 2. UnOp: mais um ramo no evaluate, com a checagem de negativo
if self.value == "!":
    if operand < 0:
        raise Exception(f"[Semantic] Factorial of negative number {operand}")
    return math.factorial(operand)

# 3. Parser: o mesmo split do extra anterior, com o laco pos-fixo
def parse_factor():
    if Parser.lexer.next.type in ("PLUS", "MINUS"):
        operator = Parser.lexer.next.value
        Parser.lexer.select_next()
        return UnOp(operator, [Parser.parse_factor()])

    result = Parser.parse_base()
    while Parser.lexer.next.type == "FACT":
        Parser.lexer.select_next()
        result = UnOp("!", [result])    # embrulha o que ja existia
    return result

Repare na diferença entre infixo e pós-fixo no laço. Num binário, você consome o operador e depois busca o operando da direita. Aqui o operando já está na mão (é o result): basta consumir o ! e embrulhar.

Casos verificados

entradasaídapor quê
-3!-6fatorial antes do unário: −(3!)
(-3)![Semantic] Factorial of negative number -3parêntese antes do fatorial
(2+1)!6o parêntese resolve primeiro
2*3!122*(3!) — fatorial antes do *
0!1caso de borda
3!+28(3!)+2
3!!720(3!)! — o while aceita encadear
!3[Parser] ... found FACTé pós-fixo, não prefixo

De novo, as duas primeiras linhas são o par que prova a precedência. -3! vale −6 (o fatorial agarra o 3 antes do sinal), e (-3)! nem chega a calcular — o parêntese fez o −3 existir antes, e aí o [Semantic] barra o negativo.

Próxima seçãoColinha · Como acrescentar um operador→
v2.0 Roteiro 5 · variáveis, blocos, Println e comentários · em Java

De calculadora a interpretador

PreProSymbolTable · VariableIdentifier · Print · Assignment · Block · NoOpASSIGN END PRINT IDENparseProgram · parseStatementevaluate(st)entrada por arquivo

O Roteiro 5 é o maior salto do curso: o compilador deixa de avaliar uma expressão e passa a executar um programa, uma instrução por linha, com variáveis que guardam valores entre uma linha e outra. A meta do roteiro:

// Comentário!
x1 = 3
y2 = 4
z_final = x1 + y2
Println(z_final)

Esta aba é em Java, com o seu código de verdade: o painel "Código" mostra o main.java da tag v2.0.2, sem nenhuma alteração. O depurador reimplementa cada método e foi conferido contra o javac: mesmas saídas e mesmas mensagens de erro.

peçav1.2 (Roteiro 4)v2.0 (Roteiro 5)
main()args[0] é a expressãoargs[0] é o arquivo · + "\n" · PrePro · cria a SymbolTable
PrePro—nasce · filter() tira os // comentários
Lexer+ - * / ( ) INT+ ASSIGN, END ('\n' vira token!), PRINT, IDEN
SymbolTable · Variable—nascem · getValue lança [Semantic]
Node.evaluateevaluate()evaluate(SymbolTable st), em TODOS os nós
Identifier · Print · Assignment · Block · NoOp—nascem
Parserrun → expressionrun → program → statement → expression

A gramática

PROGRAM    = { STATEMENT } ;
STATEMENT  = ( λ | IDENTIFIER, "=", EXPRESSION | "Println", "(", EXPRESSION, ")" ), "\n" ;
EXPRESSION = TERM, { ("+" | "-"), TERM } ;
TERM       = FACTOR, { ("*" | "/"), FACTOR } ;
FACTOR     = NUMBER | IDENTIFIER | ("+" | "-"), FACTOR | "(", EXPRESSION, ")" ;
IDENTIFIER = LETTER, { LETTER | DIGIT | "_" } ;
NUMBER     = DIGIT, { DIGIT } ;
v1.2 — a main imprime o valor da expressão
Node raiz = Parser.run(codigoFonte);
int resultado = raiz.evaluate();
System.out.println(resultado);
v2.0 — quem imprime é o nó Print
String codigoLimpo = PrePro.filter(conteudo);
Node raiz = Parser.run(codigoLimpo);
SymbolTable st = new SymbolTable();
raiz.evaluate(st);  // sem println aqui

Depurador animado — Roteiro 5

Veja o \n virar o token END na fita, o parseStatement() escolher o caminho pelo primeiro token e o Block raiz ganhar um filho por linha (inclusive os NoOp das linhas vazias). Na fase 2, a SymbolTable nasce vazia e vai sendo preenchida: a linha pisca em laranja quando é escrita e fica azul quando é lida.

Base de testes

O roteiro pede um programa com bloco de instruções, atribuições com operações entre variáveis e impressão, além dos testes de variável inválida e de variável inexistente.

programasaídao que mostra
7comentário, 3 atribuições, Println
10 · -2setValue atualiza; linha vazia vira NoOp
[Parser] Unexpected token in statement: NUMEROvariável inválida: tokens válidos, ordem errada
[Lexer] Invalid Symbol _variável inválida: '_' não começa token
[Semantic] Variable y not declaredvariável inexistente: só a execução descobre
[Parser] Expected =p minúsculo vira IDEN, e IDEN no começo da linha é atribuição
[Parser] Expected end of lineum statement por linha
NumberFormatExceptionInteger.parseInt estoura, sem prefixo

Perguntas do Roteiro 5 · treino e extra credit

Três tipos de pergunta: sobre o roteiro (conceitos), sobre as funções (o porquê de cada linha do seu código) e implemente você, no formato de extra credit e de prova: um símbolo novo, uma variável imutável… o que muda no Lexer, no Parser, na AST e na EBNF. Tente responder antes de abrir a resposta e marque ✓ sei ou ↺ revisar. As marcações ficam salvas neste navegador.

Próxima seçãoRoteiro 6 · booleanos, if, for e Scanln
v2.1 Roteiro 6 · booleanos, if, for e Scanln · em Java

A linguagem aprende a decidir e a repetir

AND OR NOTEQ GT LTIF WHILE ELSE READOPEN_BRA CLOSE_BRAIf · While · ReadparseBlockparseBoolExpression · parseBoolTerm · parseRelExpression

Até o Roteiro 5, todo programa rodava cada linha exatamente uma vez, de cima para baixo. O Roteiro 6 traz duas coisas novas: a execução passa a escolher um caminho (if/else) e a voltar para trás (for, que em Go faz o papel do while). Para isso aparecem os operadores booleanos e relacionais, as chaves de bloco e a leitura do terminal. A meta do roteiro é o fatorial:

i = 1
n = 5
f = 1
if n < 2 {
    f = 1
} else {
    for i < n || i == n {
        f = f * i
        i = i + 1
    }
}

Esta aba é em Java. O código ativo do depurador é uma referência do Roteiro 6, escrita no mesmo estilo de um main.java do Roteiro 5 (classes Lexer, Parser, Node, enum TipoToken). O enunciado pede que você escreva a sua própria EBNF: use isto para estudar, não para copiar.

peçav2.0 (Roteiro 5)v2.1 (Roteiro 6)
Lexer= ASSIGN, END, PRINT, IDEN+ lookahead de 1 caractere (== && ||), ! > < { }, if for else Scanln
BinOp / UnOp+ - * / e sinal+ == > < && || e ! (devolvem 1 ou 0)
If—nasce · 2 ou 3 filhos
While—nasce · 2 filhos
Read—nasce · 0 filhos, lê o stdin
Blocksó a raiztambém cada { … }, via parseBlock()
Parserprogram → statement → expression → term → factor+ block, boolExpression → boolTerm → relExpression por cima do expression

A gramática (uma proposta)

O diagrama da aula é o de C, com parênteses no if. Adaptado para o Go, fica assim. As linhas marcadas com ★ são novas em relação ao Roteiro 5:

PROGRAM         = { STATEMENT } ;
BLOCK           = "{", { STATEMENT }, "}" ;                              ★
STATEMENT       = ( λ | ASSIGNMENT | PRINT | IF | FOR ), "\n" ;
ASSIGNMENT      = IDENTIFIER, "=", BOOL_EXPRESSION ;
PRINT           = "Println", "(", BOOL_EXPRESSION, ")" ;
IF              = "if", BOOL_EXPRESSION, BLOCK, [ "else", BLOCK ] ;          ★
FOR             = "for", BOOL_EXPRESSION, BLOCK ;                           ★
BOOL_EXPRESSION = BOOL_TERM, { "||", BOOL_TERM } ;                        ★
BOOL_TERM       = REL_EXPRESSION, { "&&", REL_EXPRESSION } ;              ★
REL_EXPRESSION  = EXPRESSION, { ("==" | ">" | "<"), EXPRESSION } ;         ★
EXPRESSION      = TERM, { ("+" | "-"), TERM } ;
TERM            = FACTOR, { ("*" | "/"), FACTOR } ;
FACTOR          = NUMBER | IDENTIFIER | ("+" | "-" | "!"), FACTOR
                | "(", BOOL_EXPRESSION, ")" | "Scanln", "(", ")" ;      ★

A precedência sai da escada de funções, igual ao Roteiro 3: quanto mais fundo o degrau, mais forte o operador. Da mais forte para a mais fraca: ! + - (unários) → * / → + - → == > < → && → ||. Por isso 1 + 2 * 3 == 7 && !0 || 0 se lê como (((1 + (2*3)) == 7) && (!0)) || 0. Carregue esse exemplo no depurador e veja a árvore.

v2.0 — '=' é sempre ASSIGN
} else if (c == '=') {
    next = new Token(ASSIGN, 0);
    position++;
}
v2.1 — olha um caractere à frente
} else if (c == '=') {
    if (source.charAt(position + 1) == '=') {
        next = new Token(EQ, 0);  position += 2;
    } else {
        next = new Token(ASSIGN, 0); position++;
    }
}

Depurador animado — Roteiro 6

Escreva um programa (ou escolha um exemplo) e aperte ▶ Rodar. Na fita, cada token entra quando o Parser chama selectNext(); os consumidos ficam riscados e o next fica em destaque. A pilha mostra quem chamou quem: a moldura vermelha está rodando, a verde está devolvendo um valor. Na AST, os nós surgem enquanto o Parser os cria e, na fase 2, acendem conforme o evaluate() desce e mostram o valor (↩) quando sobem. O código é o método Java que está rodando, com a linha acesa. A faixa colorida embaixo da linha do tempo mostra as fases: verde é o Léxico, azul o Sintático, laranja o Semântico.

Base de testes

O roteiro pede um programa com laços e condicionais usando and/or/not e leitura do terminal. Clique numa entrada para carregá-la no depurador. As saídas abaixo foram conferidas rodando a referência Java de verdade.

programastdinsaídao que mostra
100 1 2laço com and + not; para no 3 mesmo com n = 10
7, 37condicional com and + not + Scanln
—0 1 10 11 20 21Block dentro de Block: a pilha de evaluate() fica funda
—1! é unário, mais forte que ==: (!1) == 0
—[Parser] Unexpected token in statement: ELSEo "\n" depois do } já fechou o if
—[Parser] Expected }parseBlock encontrou EOF
—[Lexer] Invalid Symbol &só existe &&
—[Parser] Unexpected token in factor: ASSIGN>= não existe: vira GT e depois ASSIGN
abc[Semantic] Scanln: invalid integer abcsó na execução se sabe o que o usuário digitou
—[Semantic] Division by zeroo BinOp avalia os DOIS filhos antes de olhar o ||

Pegadinhas

O "\n" depois do "{"

É só uma linha vazia dentro do bloco: vira um NoOp. Nenhuma regra especial é necessária, e você vê esse NoOp na AST.

O "\n" depois do "}"

Fecha o statement do if/for. Por isso o } else { precisa ficar numa linha só, que é também a regra do Go.

Booleano é int

Relacionais devolvem 1 ou 0, e If/While testam != 0. Por isso Println(3 > 2) imprime 1.

Scanln é FACTOR

Ele devolve valor, então pode aparecer no meio da conta: x = Scanln() + 1. Já o Println é statement.

While reavalia a condição

Um laço de 3 voltas avalia a condição 4 vezes. No depurador, veja volta subir na pilha.

Laço infinito

Esquecer o i = i + 1 trava o Java para sempre. O depurador para em 20 000 passos e avisa.

Perguntas do Roteiro 6 · treino e extra credit

Mesmo formato do Roteiro 5: sobre o roteiro, sobre as funções e implemente você (!= e >=, else if, for estilo C, break, repeat, tipagem forte…). Os exercícios adicionais do enunciado estão aqui.

Extra credit

if como expressão (n = if x > 2 { 5 } else { 3 }): um novo caminho no FACTOR, com a maior precedência de todas, que devolve um nó de 3 filhos (condição, valor-então, valor-senão). O evaluate dele devolve um dos dois valores, em vez de 0. for estilo C (for i = 0; i < n; i = i + 1 { … }) pode ser só açúcar sintático: o Parser monta Block[ Assignment(init), While(cond, Block[ corpo…, Assignment(passo) ]) ], sem nenhuma classe nova.

Próxima seçãoColinha · Como acrescentar um operador
Colinha

Como acrescentar um operador novo

Os três extra credits são três variações do mesmo problema, e juntos formam um procedimento. Diante de um operador novo, são três perguntas — nesta ordem.

1

O símbolo tem um caractere ou mais?

casoo que fazer no Lexerexemplo
um caractereacrescentar uma entrada em SYMBOLS^   !
dois ou maisstartswith testado antes do caractere isolado** antes de *

A ordem no segundo caso não é opcional: se o * for testado primeiro, 2**3 vira dois MULT e a potência nunca é reconhecida.

2

A precedência já existe, ou é nova?

Já existecaso do XOR

Acrescente o token à condição do while daquele nível e trate o operador no if/elif. Nenhuma função nova.

while next.type in ("PLUS", "MINUS", "XOR"):
É novacaso do ** e do !

Um nível de precedência é uma função. Para inserir um nível entre dois existentes, é preciso dividir um deles em dois, e encadear a chamada.

FACTOR → FACTOR + BASE
A regra de ouro da precedência

Quanto mais fundo na cadeia de chamadas, mais forte o operador liga. Se o operador novo precisa ligar mais forte que X e mais fraco que Y, ele vai numa função chamada por X e que chama Y. Não existe tabela de prioridade — a hierarquia de chamadas é a tabela.

3

Onde o operador fica em relação ao operando?

formaexemplono laço / na funçãonó
infixoa + bconsome o operador, depois chama o nível de baixo para pegar a direitaBinOp
prefixo-aconsome o operador antes de tudo, e chama a si mesmo (recursão)UnOp
pós-fixoa!o operando já está na mão: consome o operador e embrulha o resultadoUnOp
# infixo  — busca o operando da direita depois de consumir o operador
while next.type == "MULT":
    select_next()
    result = BinOp("*", [result, parse_factor()])

# prefixo — consome o operador e recursa no MESMO nível
if next.type == "MINUS":
    select_next()
    return UnOp("-", [parse_factor()])

# pós-fixo — o operando ja veio; so embrulha
while next.type == "FACT":
    select_next()
    result = UnOp("!", [result])
4

Como testar que a precedência ficou certa

Não basta um caso que funcione: é preciso o par que prova. Escolha duas entradas que dariam resultados diferentes se a precedência estivesse trocada:

operadorpar que provao que cada um mostra
^1+2^3 = 0  ·  2^3+1 = 2ordem de chegada, sem precedência entre eles
**-3**2 = −9  ·  (-3)**2 = 9acima do unário, abaixo do parêntese
!-3! = −6  ·  (-3)! = erroidem
E o fluxo de entrega dos extras

Cada extra vive numa branch separada, criada a partir da tag aprovada do roteiro — git checkout -b x1.2 v1.2.1 — com a tag solta na própria branch. Nunca se faz merge na main: o operador extra quebraria a verificação sintática do roteiro seguinte. Por isso a main não conhece ^, ** nem !, e cada extra desconhece os outros dois.

Parte IV

Referência completa

Todas as classes da versão atual (v1.2.1), com cada atributo e cada método, o que devolvem e quem os chama.

membrotipodevolvequem chamapapel
class Token
typeatributo · str—Parsercategoria gramatical do token
valueatributo · int | str—Parserconteúdo concreto
__init__(type, value)construtor—Lexersó guarda os dois campos
class Lexer
SYMBOLSatributo de classe · dict—select_nextmapeia caractere → tipo de token
sourceatributo · str—select_nexta entrada, guardada inteira
positionatributo · int—select_nextponteiro de leitura; a memória do cursor
nextatributo · Token—Parsero lookahead de um token
__init__(source)construtor—Parser.runzera position, next = None
select_next()métodoTokenParser (todos)pula espaços, reconhece um token, atualiza next
class Parser
lexeratributo de classe · Lexer—todos os métodosa fonte de tokens, compartilhada
run(code)estáticoNodemain()cria o Lexer, posiciona no 1º token, exige EOF no fim
parse_expression()estáticoNoderun, parse_factornível + -; monta BinOp
parse_term()estáticoNodeparse_expressionnível * /; monta BinOp
parse_factor()estáticoNodeparse_term, si mesmoINT, unário, parênteses; monta IntVal e UnOp
class Node(ABC) — abstrata
valueatributo · variant—as filhasoperador ou valor, conforme a classe
childrenatributo · list[Node]—as filhas0, 1 ou 2 filhos hoje; N no futuro
__init__(value, children)construtor—Parserherdado por todas as filhas sem alteração
evaluate()abstratoint—força cada filha a implementar o seu
class IntVal(Node) · class UnOp(Node) · class BinOp(Node)
IntVal.evaluate()overrideinto nó paidevolve self.value — o caso base da recursão
UnOp.evaluate()overrideinto nó paiavalia children[0], aplica o sinal
BinOp.evaluate()overrideinto nó pai · main() na raizavalia os dois filhos, opera; // na divisão

Quem conhece quem

Três tipos de ligação, e confundi-los é o que costuma atrapalhar. Herança é "é um tipo de". Criação é "fabrica instâncias de". Uso é "chama métodos de, sem criar".

main() run() Parser estático cria 1 · usa Lexer source · position · next cria Token type · value cria IntVal UnOp BinOp herda Node (ABC) evaluate() children são outros Node — a árvore se aninha seta cheia = cria ou chama seta vazada = herda de
O Token não conhece ninguém. O Node só conhece os próprios filhos. O Parser é a única ponte entre as duas pontas.
Para onde isso vai

O investimento do Roteiro 4

Roteiro 8: os nós ganham um generate()

Irmão do evaluate(): percorre a mesma árvore com a mesma recursão, mas em vez de devolver um número, emite instruções de assembly. É nesse momento que o interpretador vira compilador de verdade — e nada disso seria possível se a conta tivesse sido feita durante a leitura, como era até o Roteiro 3.

Variáveis: o evaluate() passa a receber um argumento

Entram um nó Identifier, um Assignment e uma tabela de símbolos. O detalhe que importa é como a tabela chega aos nós: passada como argumento — evaluate(symbol_table) — e repassada aos filhos, nunca como global. Assim, quando surgirem funções com escopo próprio, basta passar outra tabela na chamada; a árvore não muda.

Os extras não criaram classes

As branches de extra credit acrescentaram três operadores — XOR (^), potência (**) e fatorial (!) — e nenhuma classe nova. Potência e fatorial dividiram o parse_factor() em dois níveis para acomodar a precedência, e reusaram BinOp e UnOp tal como estavam. É o primeiro teste real de que a arquitetura aguenta crescer.

Parte V

A prova do colega

Dezessete questões de provas anteriores

Todas com gabarito comentado e, mais importante, com onde a nota foi perdida na correção original. O conteúdo vai de linguagens formais até o projeto do compilador.

Marque os critérios que a sua resposta cumpriria; o placar se ajusta sozinho e fica salvo no navegador.

0,0 de 31,5 comece marcando

Bloco A · Linguagens formais

Gramáticas, classes de linguagens, reconhecedores e pumping lemma.

Questão 1 3,50 pt obteve 2,45
“Linguagens são conjuntos de cadeias. E existem diversas classes de linguagens.” Discorra sobre a frase usando o conteúdo e os diagramas da disciplina. Use exemplos.
Ver resposta comentada

Comece pela definição. Dado um alfabeto Σ, o conjunto Σ* reúne todas as cadeias finitas formáveis com seus símbolos. Uma linguagem é qualquer subconjunto: L ⊆ Σ*.

Depois classifique, e diga o critério. As classes se distinguem pelo poder de máquina necessário para reconhecê-las — e se encaixam umas nas outras (hierarquia de Chomsky):

Regulares ⊂ Livres de contexto ⊂ Sensíveis ao contexto ⊂ Recursivamente enumeráveis

Então dê exemplos E explique o que cada um mostra.

  • L₁ = { abⁿ : n ≥ 0 } é regular: basta lembrar “já vi o a?”, o que cabe num número fixo de estados. Autômato finito resolve.
  • L₂ = { aⁿbⁿ : n ≥ 0 } não é regular: é preciso lembrar quantos a apareceram, e esse número não tem teto — nenhum conjunto finito de estados dá conta. É livre de contexto, e o autômato de pilha resolve porque a pilha é a memória que falta.

Essa última frase é o que amarra tudo: a classificação não é uma taxonomia arbitrária, ela mede quanta memória o reconhecimento exige.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A resposta listou os exemplos e desenhou o autômato, mas não explicou o que os exemplos significam nem por que a classe muda. O professor anotou exatamente isso ao lado do desenho. Citar não é discorrer: em questão discursiva, cada exemplo precisa vir com a frase que diz o que ele demonstra.

Questão 2 3,00 pt obteve 1,50
Explique o que são Gramáticas e qual a sua utilidade no escopo da disciplina.
Ver resposta comentada

Uma gramática formal é a quádrupla

G = ( V , Σ , P , S )

    V   conjunto de símbolos NÃO TERMINAIS (variáveis)
    Σ   conjunto de símbolos TERMINAIS
    P   conjunto de regras de produção
    S ∈ V   símbolo inicial

As regras dizem como partir de S e fazer derivações até sobrar só terminais. Por exemplo, S → aSb | ε gera { aⁿbⁿ }:

S ⇒ aSb ⇒ aaSbb ⇒ aabb

A utilidade — que é metade da nota: a gramática descreve formalmente a sintaxe de uma linguagem. No compilador, ela é o contrato que o parser implementa: ele usa a gramática para decidir se a sequência de tokens que o lexer produziu tem estrutura válida, e para construir a árvore. Cada regra da gramática vira uma função do parser — EXPRESSION, TERM, FACTOR. Sem gramática não há critério objetivo de “programa válido”.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

Dois problemas. Primeiro, V foi definido como “o vocabulário da linguagem” — é o conjunto de não terminais. Segundo, e mais caro: a utilidade foi resolvida em uma linha (“definir uma única linguagem”). A questão vale 3,00 e a palavra utilidade está no enunciado: metade da nota está em ligar a gramática ao trabalho concreto do parser.

Questão 3.a 0,50 pt obteve 0,30
Seja L = { ambn : m,n ≥ 0 } ∪ { bman : m,n ≥ 0 }. Liste 5 cadeias possíveis de L.
Ver resposta comentada

O critério é simples e vale decorar: a cadeia pode ter no máximo uma troca de símbolo. Ou é um bloco de a seguido de um bloco de b, ou o contrário.

ε          (m = 0, n = 0)
a
bbb
aaabb
bbbaaaa

Teste você mesmo — o campo aceita qualquer cadeia de a e b:

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

abba foi marcada como errada, e está mesmo: ela troca de símbolo duas vezes (a→b→a). Nem a*b* nem b*a* aceitam isso. A pegadinha é ler a união como “qualquer mistura de a e b” — não é: é um bloco seguido de outro bloco, e só.

Questão 3.b 1,00 pt obteve 0,00
Faça um reconhecedor adequado para a linguagem L acima.
Ver resposta comentada

Não precisa de pilha. Repare que o enunciado usa m e n independentes — não exige que sejam iguais. Logo:

L = a* b*  |  b* a*

Isso é uma expressão regular, então um autômato finito basta. Convenção dos diagramas: círculo duplo = estado de aceitação; a seta início marca o estado inicial.

Versão 1 · AFN. Do estado inicial saem duas transições ε (sem consumir nada): o autômato “escolhe” em qual ramo apostar. Em cima fica a*b*, embaixo b*a*.

inícioεεbaabbaq0q1q2q3q4a*b*b*a*
  • q1: só viu a até agora — aceita, porque aⁿ ∈ L.
  • q2: viu a’s e depois b’s. Daqui em diante só pode vir b; um a não tem transição e a cadeia morre nesse ramo.
  • q3 e q4: o mesmo raciocínio, espelhado.
  • A cadeia vazia é aceita porque, pelas ε, q0 já alcança estados de aceitação.

Versão 2 · AFD (se a questão pedir determinístico). Sem ε: é a primeira letra que escolhe o ramo. Aparece um estado novo, q5, o estado morto — para onde vai toda cadeia que já trocou de símbolo duas vezes. Ele não é de aceitação e não tem saída.

inícioabbaababbaa, bq0q1q2q3q4q5
estadoo que ele “lembra”lendo alendo baceita?
→ q0nada lido aindaq1q3sim (ε)
q1só a’sq1q2sim
q2a’s e depois b’sq5q2sim
q3só b’sq4q3sim
q4b’s e depois a’sq4q5sim
q5trocou de símbolo duas vezesq5q5não

Repare: os estados guardam só em que fase a leitura está, nunca quantas letras vieram. Seis estados bastam para qualquer tamanho de cadeia — é exatamente isso que faz a linguagem ser regular. Para aⁿbⁿ seria preciso um estado por valor de n, e aí não há autômato finito que dê conta.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A resposta propôs um autômato de pilha não determinístico — máquina forte demais para o problema. O erro nasce de ler ambn como se fosse anbn. Sempre confira se os expoentes são o mesmo símbolo: essa única letra decide entre regular e livre de contexto.

Questão 3.c 1,00 pt marcada 1,00 — ver ressalva
“Essa linguagem é Livre de Contexto, mas não é Regular.” Concorde ou discorde. Justifique.
Ver resposta comentada

Pela linguagem como está impressa no enunciado, a resposta correta é discordo:

  • L = a*b* ∪ b*a* é regular — tem expressão regular, logo tem autômato finito.
  • Toda linguagem regular também é livre de contexto (a inclusão é própria, mas vale). Então “é livre de contexto” não é falso; o que é falso é o “mas não é regular”.

A afirmação só seria verdadeira se a linguagem fosse { aⁿbⁿ }, com o mesmo expoente dos dois lados.

Ressalva importante

Esta questão recebeu nota cheia na correção original, com a justificativa de que “os a e b têm o mesmo número” — o que não é o que o enunciado impresso diz. Há inconsistência entre o enunciado fotografado e a correção. Não memorize a justificativa dada: memorize o critério. Se os expoentes forem independentes, é regular; se forem amarrados, é livre de contexto.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

Aqui a armadilha é de leitura, não de conceito. A justificativa escrita (“o mesmo número de a e de b”) descreve uma linguagem diferente da que está no enunciado. Numa prova, releia os índices antes de decidir a classe.

Questão 3.d 1,00 pt obteve 1,00
“Para provar que ela é Regular, bastaria fazer um Pumping Lemma.” Concorde ou discorde. Justifique.
Ver resposta comentada

Discordo. O lema do bombeamento é uma condição necessária, não suficiente:

L regular            ⇒      L satisfaz o pumping lemma     ✓
L satisfaz o lema    ⇒/     L regular                      ✗

Por isso ele é usado por contradição, para provar que algo NÃO é regular: você supõe regular, aplica o lema, e chega a uma palavra bombeada que sai da linguagem.

Para provar que é regular, os caminhos válidos são construtivos: exibir uma expressão regular, um AFD/AFN, uma gramática regular, ou usar propriedades de fechamento a partir de linguagens sabidamente regulares.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

Nada perdido aqui — a resposta acertou o raciocínio. Vale apenas reforçar a formulação: o problema não é o lema “ser inconclusivo”, é que a implicação só vale num sentido.

Questão 1.d 1,00 pt obteve 1,00
Considere a função f(x) = x + 3 em unário, representada pela linguagem L = { 1n # 1n+3 : n ≥ 0 } — a entrada, um separador, e a saída. “L pode ser representada por uma Linguagem Livre de Contexto.” Concorde ou discorde. Justifique.
Ver resposta comentada

Concordo. E dá para provar das duas formas que a disciplina ensina — com gramática e com autômato.

Pela gramática. Cada 1 da esquerda precisa de um 1 correspondente na direita, e o separador vem acompanhado de três 1 extras:

S → 1 S 1  |  # 1 1 1

A primeira regra gera os 1 aos pares, um de cada lado; a segunda fecha com o separador e os três a mais. Para n = 2:

S ⇒ 1S1 ⇒ 11S11 ⇒ 11#11111        (2 à esquerda, 5 à direita)

O lado esquerdo de toda regra é um único não-terminal: é livre de contexto.

Pelo autômato de pilha. Empilha um X por 1 antes do #; ao ler o #, empilha mais três; depois desempilha um por 1. Aceita com pilha vazia. Rode:

Pilha

E ela não é regular: é preciso lembrar quantos 1 vieram antes do # para conferir a direita, e esse número não tem teto — nenhum conjunto finito de estados dá conta. Então a resposta completa é: livre de contexto, e não regular.

O que uma resposta de nota máxima precisa ter
O que dá para aprender com a resposta original

O colega escreveu “colocar 3 1’s na pilha fixos e, para cada 1 de entrada, colocar 1 na pilha” — e levou nota cheia, com a anotação “ok, apesar de 1a estar errado”. A intuição estava certa: é exatamente o autômato acima, que empilha três fixos no separador. O que faltou foi formalizar: dizer qual é a linguagem, mostrar que o reconhecimento acaba com a pilha vazia, e justificar por que um autômato finito não bastaria.

Questão extra +1,00 pt obteve 0,00
Prove formalmente que L = { wcw : w ∈ {a,b}* } não é Livre de Contexto.
Ver resposta comentada

Use o pumping lemma para CFL. O passo decisivo é escolher uma palavra que realmente pertença a L.

  1. Suponha L livre de contexto. Existe então o comprimento p do lema.
  2. Escolha w = apbp, logo z = apbp c apbp. Confira: z ∈ L, porque as duas metades em volta do c são idênticas.
  3. O lema garante z = uvxyz' com |vxy| ≤ p e vy ≠ ε.
  4. Como |vxy| ≤ p, esse trecho é curto demais para tocar as duas metades e o c ao mesmo tempo. Dois casos: ou ele fica inteiro de um lado do c, ou engloba o c mas só a vizinhança dele.
  5. Bombeando com i = 2: no primeiro caso uma metade cresce e a outra não, então deixam de ser iguais. No segundo, ou o c se duplica (e a palavra nem tem a forma wcw), ou o fim da primeira metade e o começo da segunda mudam de tamanhos diferentes.
  6. Em todos os casos uv²xy²z' ∉ L. Contradição — L não é livre de contexto.
O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A tentativa escolheu z = akbkck — que nem pertence a L. Em wcw existe um c, no meio, e as duas metades são cópias. Se a palavra escolhida não está na linguagem, a prova inteira desmorona antes de começar. Primeiro passo de todo pumping: verificar que z ∈ L.

Bloco B · Compiladores

Fases de compilação, implementação de operadores, AST e classes de linguagem.

Questão 1 4,00 pt obteve 1,00
Usando como base a v2.0 do compilador, explique passo a passo como implementaria o operador >= entre duas expressões. A desigualdade teria a menor precedência possível e retornaria 1 (true) ou 0 (false). Explore detalhadamente todas as etapas de compilação.
Ver resposta comentada

A palavra que vale os 4 pontos é detalhadamente. Uma fase por vez, com o que muda em cada uma:

1 · Lexer. Token novo GE. Testar >= antes de > sozinho, senão o lexer devolve dois tokens errados (mesma armadilha do ** no extra credit).

x >= 4 + 1   →   IDEN(x) GE INT(4) PLUS INT(1)

2 · Gramática e parser. Menor precedência significa um nível NOVO acima do atual topo. O que hoje é EXPRESSION passa a ser chamado por uma camada relacional:

RELEXPR    = EXPRESSION, { ">=", EXPRESSION } ;
EXPRESSION = TERM, { ("+"|"-"), TERM } ;

Assim x >= 4+1 agrupa como x >= (4+1), e não (x>=4)+1. Quem chamava parse_expression() passa a chamar parse_relexpr().

3 · AST. Nenhuma classe nova: é uma operação binária, então reusa BinOp com value = ">=". Para x >= 4+1:

      BinOp('>=')
       /        \
  Identifier    BinOp('+')
     (x)        /       \
            IntVal(4) IntVal(1)

4 · Semântico. O evaluate() do BinOp ganha o ramo >=, devolvendo 1 ou 0. É aqui que entra a checagem de tipos: comparar inteiro com string deve levantar [Semantic]. A tabela de símbolos resolve o Identifier x.

5 · Geração de código. Avalia os dois lados, compara e materializa o booleano como inteiro:

  MOV EAX, esquerda
  CMP EAX, direita
  JGE verdadeiro
  MOV EAX, 0
  JMP fim
verdadeiro:
  MOV EAX, 1
fim:

6 · Testes. 5>=5 → 1 · 6>=5 → 1 · 4>=5 → 0 · 2 >= 4+1 → 0 (este é o que prova a precedência).

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A resposta nomeou as fases (“adicionaria no lexer, depois no parser, depois na symbol table”) sem dizer o que muda em cada uma. Duas falhas específicas: não tratou a precedência, que é o coração do enunciado, e citou a tabela de símbolos como se ela guardasse os valores 0 e 1 — ela guarda variáveis, não resultados de comparação. Em questão de 4 pontos que diz “detalhadamente”, cada fase precisa de um exemplo concreto.

Questão 2 3,00 pt obteve 3,00
Explique em quais etapas de compilação o seu compilador dispara erros (exceções). Dê exemplos de entradas inválidas para cada etapa.
Ver resposta comentada

Resposta modelo — esta questão saiu com nota cheia. A estrutura que funciona é uma pergunta por fase:

etapaa pergunta que ela fazexemplo que falhaprefixo
Léxica“esses caracteres formam tokens?”@ x = 10;[Lexer]
Sintática“esses tokens estão numa ordem válida?”x = 10[Parser]
Semântica“essa estrutura faz sentido?”x = 10 + "Raul";[Semantic]

O detalhe que dá força à resposta: nos três exemplos, o erro anterior não aconteceu. Em x = 10 todos os tokens são válidos — só falta o ;. Em 10 + "Raul" os tokens e a ordem estão perfeitos — o problema é o significado. Mostrar essa progressão prova que você entendeu a separação de fases, não só decorou a lista.

O que uma resposta de nota máxima precisa ter
Questão 3.a parte de 3,00 pt obteve 0,75
“O projeto do compilador implementa uma sintaxe que pode ser representada por uma Linguagem Sensível ao Contexto.” Verdadeiro ou falso? Justifique.
Ver resposta comentada

Verdadeiro, mas pelo motivo certo — e com a ressalva que vale o resto da nota.

A inclusão da hierarquia é própria mas vale para cima: Regulares ⊆ LC ⊆ LSC. Como a sintaxe do compilador é descrita por uma gramática livre de contexto, ela também pertence à classe das sensíveis ao contexto. Toda LC é LSC.

A ressalva: isso não significa que seja necessária uma LSC. Uma gramática livre de contexto é suficiente para a parte sintática, e é o que o projeto usa. Dizer “pode ser representada” é diferente de “precisa ser”.

Onde o compilador realmente esbarra em sensibilidade ao contexto é na fase semântica: “toda variável usada foi declarada” não é expressável por GLC. Mas isso não se resolve com uma gramática mais forte — resolve-se com tabela de símbolos.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A cadeia de inclusão estava certa, mas faltou a ressalva: o enunciado pergunta se pode ser representada, e responder só isso deixa metade da questão de fora. Diga também o que é suficiente.

Questão 3.b parte de 3,00 pt obteve 0,00
“A AST gerada pelo Parser é uma árvore de derivação não-ambígua.” Verdadeiro ou falso? Justifique.
Ver resposta comentada

Falso. AST e árvore de derivação são coisas diferentes:

árvore de derivaçãoAST
contémtodo não-terminal aplicado: EXPRESSION, TERM, FACTOR, NUMBERsó operadores e valores
provaque a cadeia obedece à gramáticanada — ela já assume isso
serve paraa análise sintáticaavaliar e gerar código
parêntesesaparecem como terminaisdesaparecem

E a segunda metade da afirmação também não se sustenta: quem elimina a ambiguidade é a gramática, estratificada em EXPRESSION/TERM/FACTOR, ou regras explícitas de precedência e associatividade. A AST é consequência de uma gramática não ambígua, não a causa dela. Uma gramática ambígua produziria ASTs diferentes para a mesma entrada — a árvore não teria como se defender.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A resposta afirmou verdadeiro e justificou com “a profundidade dos tokens tira a ambiguidade”. Dois erros num só: tratou AST e árvore de derivação como sinônimos, e inverteu a causalidade — a profundidade reflete a precedência que a gramática já definiu; ela não a cria.

Bloco C · Projeto e paradigmas

Escopo, evolução do projeto, tipos, paradigmas e otimizações.

Questão 1 3,00 pt obteve 2,75
Explique como foi implementado o escopo local de variáveis no projeto.
Ver resposta comentada

Cada escopo tem a sua própria tabela de símbolos, e cada tabela guarda uma referência para a tabela do escopo que a contém (parent).

  • Ao entrar numa função ou num bloco { ... }, cria-se uma tabela filha apontando para a atual.
  • Ao declarar, a variável entra apenas na tabela atual.
  • Ao buscar, procura-se na tabela atual; se não achar, sobe para o parent, e assim até a global. Se chegar ao topo sem achar, é erro semântico.

Essa cadeia de busca é o que produz o shadowing:

let x = 5;
{
    let x = 10;
    print(x);     // 10 - acha na tabela local e para
}
print(x);         // 5  - a tabela local morreu ao fechar o bloco

Além do valor, a tabela guarda o tipo e a posição na pilha — esta última é o que a geração de código usa para emitir [EBP-4], [EBP-8] e assim por diante.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

Quase nota cheia. O que faltou foi fechar o ciclo: além de criar a tabela filha, dizer que ela é descartada ao sair do bloco — é isso que faz o x externo voltar a valer 5.

Questão 2 3,00 pt obteve 3,00
Explique detalhadamente as principais diferenças de implementação entre a v2.3 e a v3.0 do projeto.
Ver resposta comentada

Resposta modelo — saiu com nota cheia. O eixo é: v2.3 acrescenta funções; v3.0 troca interpretar por compilar.

v2.3 · funções. Tokens e palavras reservadas novas (fn, return); o parser ganha a estrutura de declaração e chamada; a AST ganha nós FuncDec, FuncCall, Block e Return. Cada chamada cria uma tabela de símbolos nova — é aqui que o escopo da questão anterior vira essencial.

v3.0 · geração de assembly. O evaluate() ganha um irmão, generate(), que percorre a mesma árvore mas emite instruções em vez de calcular. As mudanças concretas:

  • variáveis deixam de viver num dicionário e passam a ter posição na pilha, acessadas como [EBP-4];
  • EAX vira o registrador de trabalho para o resultado de cada expressão;
  • condicionais e laços viram CMP + JE/JMP com rótulos únicos;
  • chamadas de função exigem prólogo e epílogo (PUSH EBP / MOV EBP, ESP / POP EBP).

A frase que amarra: a árvore não muda. O que muda é o que se faz ao percorrê-la.

O que uma resposta de nota máxima precisa ter
Questão 3 2,00 pt obteve 0,00
Discorra sobre Paradigmas de Programação.
Ver resposta comentada

Paradigma é um modelo de organizar e pensar a construção de programas — não tem relação com velocidade nem com a forma de execução.

paradigmaideia centrallinguagens
Imperativo / proceduralsequência de comandos que modificam estadoC, Pascal
Orientado a objetosobjetos que combinam estado e comportamentoJava, C#, C++
Funcionalfunções, composição, imutabilidade, sem efeitos colateraisHaskell, Lisp
Lógico / declarativodescreve relações e deixa a máquina buscar a soluçãoProlog
// imperativo: eu digo COMO fazer
soma = 0;
for (i = 0; i < 10; i++) soma += i;

// funcional: eu digo O QUE quero
soma = reduce(add, range(0, 10))

Vale acrescentar que a maioria das linguagens modernas é multiparadigma: Python permite procedural, OO e funcional no mesmo arquivo. E citar o próprio projeto: o compilador foi escrito em Python orientado a objetos (a hierarquia Node é polimorfismo puro), e compila uma linguagem imperativa.

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A resposta falou de compilado × interpretado e de desempenho. Isso é uma classificação de como a linguagem é executada, não de paradigma — C e Haskell são ambas compiladas e estão em paradigmas opostos. Zerou por responder outra pergunta. Quando o enunciado é uma palavra-chave do vocabulário da disciplina, comece definindo a palavra.

Questão 4 2,00 pt obteve 2,00
Qual foi o impacto em cada etapa de compilação com a adoção de tipos na linguagem compilada?
Ver resposta comentada

Resposta modelo — nota cheia. A estrutura vencedora é descer a pipeline dizendo o que cada fase ganhou:

Lexer         reconhece as palavras de tipo como tokens
              (let, mut, i32, str, bool) — via dicionário de keywords
     ↓
Parser        a gramática aceita declarações tipadas:
              let mut x : i32 = 5 ;
     ↓
Symbol Table  passa a guardar o TIPO junto do valor
     ↓
Semântico     verifica compatibilidade nas operações
     ↓
Codegen       escolhe a instrução adequada ao tipo

A tabela de símbolos vira o centro da coisa:

idtipovalor
xbooltrue
yi323

E o exemplo que fecha: x = 10 + "Raul" passa pelo léxico e pelo sintático sem problema — só o semântico, consultando os tipos, consegue reprovar.

O que uma resposta de nota máxima precisa ter
Questão extra +1,00 pt obteve 0,00
Proponha otimizações no projeto do interpretador. Justifique em que etapa poderiam ser aplicadas e por que preservam o comportamento do programa.
Ver resposta comentada

A otimização canônica para esse projeto é constant folding (dobramento de constantes): resolver em tempo de compilação tudo que não depende de dados de execução.

x = 3 + 4 * 2;

              +                         a AST é reescrita
             / \          →            para:
            3   *
               / \                      IntVal(11)
              4   2

Em que etapa: sobre a AST, depois do parsing e preferencialmente depois das verificações semânticas — se você dobrar antes, perde a chance de reportar erro de tipo em uma subexpressão que sumiu.

Por que preserva o comportamento: as operações envolvidas são puras — não leem nem alteram estado, não dependem de entrada, não têm efeito colateral. 3+4*2 e 11 são indistinguíveis para o resto do programa. O cuidado: a aritmética do compilador precisa seguir exatamente a semântica da linguagem-alvo (mesma divisão inteira, mesmo overflow), senão a otimização muda o resultado.

Outras respostas aceitáveis: propagação de constantes, eliminação de código morto, eliminação de subexpressões redundantes, simplificações algébricas seguras (x*1, x+0).

O que uma resposta de nota máxima precisa ter
Onde a nota foi perdida

A proposta foi “criar uma AST no lexer” para paralelizar. Não existe: a AST é produto do parser, porque só ele conhece a estrutura sintática. O lexer vê uma fita de caracteres e não tem como saber que 4*2 agrupa antes de 3+. Antes de propor otimização, confirme qual fase produz o quê.

As lições que se repetem

Olhando as dezessete correções juntas, os pontos perdidos caem em cinco padrões. Eles valem mais que qualquer conteúdo específico.

1 · Citar não é discorrerQ1 e Q2 do bloco A, Q1 do bloco B

Listar exemplos ou nomear as fases não pontua. Cada item precisa vir com a frase que diz o que ele demonstra. Em questões de 3 ou 4 pontos, conte quantas ideias distintas o enunciado pede — normalmente é uma por ponto.

2 · Leia os índicesQ3.b e Q3.c

ambn e anbn são linguagens de classes diferentes. Uma única letra decide entre regular e livre de contexto, entre autômato finito e pilha. Circule os expoentes antes de responder qualquer coisa.

3 · Pumping só prova negativaQ3.d e a extra

Ele é condição necessária. Serve para provar que algo não pertence à classe, por contradição. E o primeiro passo de toda aplicação é verificar que a palavra escolhida pertence à linguagem — foi o que derrubou a questão extra.

4 · Não confunda os artefatosQ3.b do bloco B, extra do bloco C

AST ≠ árvore de derivação. AST nasce no parser, nunca no lexer. Tabela de símbolos guarda variáveis, não resultados. Saber qual fase produz o quê evita três zeros diferentes.

5 · Defina a palavra do enunciadoQ3 do bloco C

“Discorra sobre paradigmas” zerou porque a resposta falou de compilado × interpretado. Quando o enunciado traz um termo do vocabulário da disciplina, a primeira frase tem que ser a definição dele.

√ O que funcionouas quatro questões com nota cheia

Todas seguiram o mesmo formato: uma tabela ou pipeline percorrendo as fases, com um exemplo concreto por linha, e uma frase final amarrando. Erros de compilação, v2.3×v3.0 e tipos saíram com 100% assim.

Revisão

Os meus erros no simulado

Dez questões erradas, mas seis ideias por trás delas. Errar a mesma ideia de dois jeitos diferentes é o que mais custa na prova. Por isso a revisão é organizada pela ideia, e não pela questão.

Em cada bloco: tente responder as perguntas antes de abri-las, leia a regra e faça os testes rápidos. Depois, refaça o bloco novo do simulado sobre os extras.

1

Quem pega o erro?

  • Q02"1 1" é rejeitada pelo Lexer? — Falso
  • Q051/0 dá erro [Parser]? — Falso

Por que a armadilha pega: a gente associa “entrada errada” com a primeira fase que lê a entrada. Mas cada fase só consegue ver um tipo de problema, e só reclama do que consegue ver.

Pense antes de abrir
Em 1 1, existe algum caractere que o Lexer não conheça?
Não. 1, espaço, 1: tudo conhecido. O Lexer devolve INT(1) INT(1) EOF tranquilamente. Ordem não é assunto dele.
Então quem percebe que sobrou um número? Em que linha do código?
O run(), depois de parse_expression() voltar: if Parser.lexer.next.type != "EOF". Sobrou um INT → [Parser] Unexpected token INT.
Em 1/0, o Parser consegue saber que o divisor é zero? E se a entrada fosse 1/(2-2)?
O Parser só vê a forma: número, barra, número. Em 1/(2-2) o zero nem está escrito: ele só existe depois de calcular. Quem calcula é o evaluate(), por isso o erro é [Semantic].
Leve para a prova
fasea única pergunta que ela fazexemplo que ela barra
[Lexer]esse caractere existe na linguagem?2 $ 3
[Parser]essa sequência de tokens está numa ordem permitida?1 1 · (2+3 · 1--
[Semantic]essa conta faz sentido quando executada?1/0 · (-3)!
Confira agora

Quem rejeita (2+3 ?

Quem rejeita (-3)! no x1.2?

Quem rejeita 1+2*3 no x1.0?

Rever a v1.2 e as três fases →
2

A divisão do Python arredonda para baixo

  • Q033 - -2/4 — resposta 4

Por que a armadilha pega: na cabeça, -0,5 “vira 0”, porque é assim que calculadoras e C/Go truncam. Mas o compilador usa //, e o // do Python não trunca.

Pense antes de abrir
-2/4 é -0,5. Na reta numérica, “para baixo” leva a 0 ou a −1?
A −1. “Para baixo” é em direção ao menos infinito, e não em direção ao zero. -2 // 4 == -1, então 3 - (-1) = 4.
Quando truncar e arredondar para baixo dão o mesmo resultado?
Com resultado positivo: 7 // 2 = 3 dos dois jeitos. A diferença só aparece com negativos. Por isso a pegadinha sempre coloca um sinal de menos.
Leve para a prova

// = piso (floor). Resultado negativo não exato → vai para o inteiro menor. -7//2 = -4, e não -3.

Confira agora

-7/2 na v1.2 vale:

7/-2 na v1.2 vale:

7/2 na v1.2 vale:

Rever a divisão na v1.1 →
3

Precedência é onde o operador mora

  • Q061+2^3 no x1.0 — resposta 0
  • Q08-3! no x1.2 — resposta −6

Por que a armadilha pega: a gente traz a precedência da matemática (ou do Python) para dentro do nosso compilador. Mas aqui a precedência é só uma coisa: em qual função e em qual laço o operador foi colocado.

Pense antes de abrir
No x1.0, em qual while o ^ está? Quem mais está nesse while?
No while de parse_expression(), junto de + e -. Mesmo while = mesma precedência = da esquerda para a direita: (1+2)^3 = 3^3 = 0.
Em -3!, qual função vê o - primeiro? O que ela chama logo depois?
parse_factor() vê o MINUS e chama parse_factor() de novo. Esse segundo parse_factor() lê o 3, vê o ! e devolve 3! = 6. Só então o primeiro aplica o menos: −6.
E o que é preciso escrever para o menos ser aplicado antes do !?
Parênteses: (-3)!. Aí o −3 existe como BASE antes do fatorial, e o [Semantic] barra.
Leve para a prova
EXPRESSION+   −   ^ (x1.0)liga menos forte
↓ chama
TERM*   /
↓ chama
FACTORunário + −, depois BASE com o laço de ** (x1.1) ou ! (x1.2)
↓ chama
BASEnúmero · ( EXPRESSION )liga mais forte

Mais fundo = liga mais forte. Mesma função = mesma precedência = esquerda para a direita. Repare que o ** e o ! ficam abaixo do unário: por isso -2**2 = -4 e -3! = -6.

Confira agora

2*3! no x1.2 vale:

-2**2 no x1.1 vale:

2^3+1 no x1.0 vale:

Rever os extras no roteiro v1.1 →
4

O que cada peça sabe

  • Q07Por que o Lexer testa ** antes de *? — para não virar dois MULT
  • Q09O fatorial exigiu classe nova na AST? — Falso

Por que a armadilha pega: parece natural que o ** “ganhe prioridade” no Lexer por ter prioridade na conta, e que um operador novo mereça um nó novo. Nos dois casos se atribui a uma peça uma responsabilidade que é de outra.

Pense antes de abrir
Se o Lexer testasse * primeiro, que tokens sairiam de 2**3?
INT(2) MULT MULT INT(3). O Parser receberia 2 * * 3 e daria erro. Isso é problema de cortar o texto, não de ordem de conta. Regra do Lexer: tente sempre o token mais longo primeiro.
Quantos operandos tem o !? Qual classe já existe com exatamente um filho?
Um operando → UnOp, com value = "!". As classes da AST são separadas pela quantidade de filhos, e não por qual operador é: IntVal (0), UnOp (1), BinOp (2).
Leve para a prova

Lexer: onde um token começa e termina. Nunca precedência. Parser: precedência e associatividade, pela posição nas funções. AST: uma classe por aridade. evaluate(): o que cada operador faz.

Confira agora

Implementando >=, o Lexer deve testar:

Um operador % binário precisa de classe nova?

Qual peça decide que o fatorial liga mais forte que o unário?

Rever a colinha de como acrescentar um operador →
5

Fechamento: as livres de contexto não fecham tudo

  • Q14L₁, L₂ livres de contexto ⇒ L₁ ∩ L₂ livre de contexto? — Falso
  • Q26Livres de contexto são fechadas sob complemento? — Falso

Por que a armadilha pega: as regulares fecham sob tudo, e é fácil generalizar. Mas a pilha tem um limite: ela consegue casar um par de contagens, não dois ao mesmo tempo.

Pense antes de abrir
Em {aⁿbⁿcᵐ}, o que precisa casar? E em {aᵐbⁿcⁿ}?
Na primeira, os a’s com os b’s (o c é livre). Na segunda, os b’s com os c’s. Cada uma casa um par: a pilha resolve, as duas são livres de contexto.
E uma cadeia que está nas duas ao mesmo tempo precisa cumprir o quê?
Os dois pares: a = b e b = c. Isso é {aⁿbⁿcⁿ}, que o pumping lemma mostra não ser livre de contexto. A interseção saiu da classe.
Se as livres de contexto fechassem sob complemento, o que daria para provar sobre a interseção?
Por De Morgan, L₁ ∩ L₂ = ¬(¬L₁ ∪ ¬L₂). A união elas já fecham. Se fechassem sob complemento, fechariam sob interseção, o que acabamos de ver que é falso. Logo não fecham sob complemento.
Leve para a prova
operaçãoregulareslivres de contexto
união, concatenação, estrelafechamfecham
interseção com uma regularfechamfecham — a exceção
interseção geralfechamnão fecham
complementofechamnão fecham
Confira agora

Livre de contexto ∩ regular é sempre livre de contexto?

O complemento de uma linguagem regular é regular?

A união de duas livres de contexto é livre de contexto?

Rever a parte de Fechamento →
6

LL ou LR: não é o número, é a direção

  • Q21O parser é LR(1) porque olha um token à frente? — Falso, é LL(1)

Por que a armadilha pega: o “olhar um token à frente” é real, e salta aos olhos no código (o lookahead). Mas esse é o número entre parênteses, e vale igual para LL e LR.

Pense antes de abrir
Nosso parser começa chamando qual função? Isso é partir do símbolo inicial ou da cadeia?
Começa em parse_expression(), o símbolo inicial, e vai expandindo até chegar aos tokens. De cima para baixo: top-down. Isso é LL.
Onde estão o shift e o reduce no nosso código?
Em lugar nenhum. Não existe pilha de símbolos explícita nem reduções. Quem tem shift/reduce é o LR, que parte dos tokens e vai reduzindo até o símbolo inicial (bottom-up).
Leve para a prova

L R ( 1 ) se lê assim: 1ª letra = lê a entrada da esquerda para a direita (os dois fazem isso) · 2ª letra = L: derivação mais à esquerda, top-down; R: derivação mais à direita ao contrário, bottom-up · número = quantos tokens de lookahead.

Confira agora

Descida recursiva (uma função por regra) é:

Um parser que empilha tokens e troca o topo da pilha pelo lado esquerdo de uma regra é:

“k = 1” indica que o parser é LL?

Rever a Aula 9 · LL, LR, limites →
Parte VI

Simulado · 54 questões

10 questões sobre o nosso código, 20 no estilo da prova e, no fim, 24 sobre como os extras foram implementados — todas conferidas rodando o código de cada tag. Há pegadinhas espalhadas — o selo só aparece depois que você responde, para não entregar o jogo.

Nas de verdadeiro ou falso, pense na justificativa antes de clicar: na prova, V/F sem justificativa vale zero. Para as questões de código, a colinha tem o código completo de todas as versões.

0acertos
0/54respondidas
0pegadinhas
em que caiu
Mostrar
Parte VII

Flashcards

Os conceitos que mais caem, um por cartão. Clique (ou aperte espaço) para virar, e marque se já sabe ou se precisa revisar — o filtro só os que revisar monta a próxima rodada com o que ficou.

clique ou espaço para ver a resposta
← → para navegar
Parte VIII

Colinha · todo o código

O main.py completo de cada tag, incluindo os três extras. No modo o que mudou, cada versão aparece como diff em relação à anterior — e nos extras, em relação ao roteiro de onde a branch saiu. Linhas verdes entraram; vermelhas saíram.

Primeiro vem o código, com a lista peça por peça ao lado explicando cada classe e cada função. Clique numa peça para destacar as linhas dela. Logo abaixo fica a descrição da versão: o que o programa faz, o que mudou, a gramática e exemplos de execução, todos gerados rodando aquele código.

Depois da descrição, cada versão mostra a gramática regra por regra com o diagrama sintático de cada regra (círculo ou pílula = token consumido, retângulo = chamada de outra regra), as árvores de um exemplo (a de derivação, gerada instrumentando o parser real, e a AST quando existe) e as classes e como se relacionam, extraídas do código.

Códigoclique numa peça à esquerda para destacar as linhas
Animação: como as funções são chamadasa execução real deste código, passo a passo
Descrição desta versãoo que faz, o que mudou, gramática, diagramas, árvores e classes