Exercícios — Análise Sintática
Os exercícios assinalados com "✓" serão resolvidos nas aulas práticas; Os assinalados com "†" têm elevada dificuldade. Todos os restantes devem ser resolvidos pelos alunos.
- Exercícios — Análise Sintática
Limpeza de uma Gramática
Exercício 01
Considere a gramática independente do contexto com produções
- Escreva uma expressão regular que represente a linguagem gerada por .
- Construa uma gramática (essencialmente) não contraível equivalente.
- Elimine as produções unitárias da gramática obtida na alínea anterior.
Exercício 02
Repita o exercício anterior para a gramática:
-
com produções
-
com produções
Exercício 03
Construa uma gramática equivalente a , com produções que não contenha produções unitárias e escreva uma expressão regular que represente a linguagem gerada por esta gramática.
Exercício 04
Construa uma gramática equivalente a , com produções sem símbolos inúteis e encontre uma expressão regular que represente a linguagem gerada por esta gramática.
Exercício 05
Construa uma gramática equivalente a , com produções na forma normal de Chomsky.
Exercício 06
✓ Construa uma gramática equivalente a , com produções na forma normal de Greibach.
Exercício 07
Repita o exercício anterior para a gramática
- Com produções
- Com produções
Gramáticas LL(k)
Exercício 08
Calcule os símbolos diretores e determine se as seguintes gramáticas são :
- ✓
Exercício 09
†✓ Modifique a gramática de modo a obter uma gramática equivalente. As produções de são: Sugestão: Comece por determinar .
Exercício 10
Repita o exercício anterior para a gramática
Exercício 11
Defina uma gramática que gere a linguagem das expressões aritméticas com subtração, multiplicação e parêntesis. (Use o símbolo para representar os inteiros.)
Gramáticas LR(0)
Exercício 12
Determine os da gramática com produções
- ✓
Exercício 13
✓ Considere a gramática .
- Construa o autómato dos itens válidos de .
- Diga, justificando, se é .
- Construa a tabela de análise sintática para .
- Defina o autómato de pilha reconhecedor para .
Exercício 14
Repita as duas primeiras alíneas do exercício anterior para a gramática Se concluir que a gramática não é , enumere os conflitos encontrados.
Exercício 15
Repita o exercício anterior para a gramática
Gramáticas LR(1)
Exercício 16
✓ Considere a gramática , com o conjunto de produções:
- Construa o autómato dos itens válidos de .
- Diga, justificando, se é .
- Construa o autómato amalgamado e diga, justificando, se é .
- Construa a tabela de análise sintática para .
- Com base no autómato dos itens válidos de , defina o autómato de pilha reconhecedor .
- Construa a computação do autómato de pilha para a palavra .
Resolução em Banda Desenhada
AIV | TAS | APR |
---|---|---|
![]() | ![]() | ![]() |
Exercício 17
✓ Repita o exercício anterior com a gramática , com o conjunto de produções:
Exercício 18
✓ Repita as primeiras 4 alíneas do exercício anterior com a gramática , com o conjunto de produções: (Se não é , não faça as alíneas que não se aplicam.)
Exercício 19
Repita o exercício anterior com as gramáticas
- , com o conjunto de produções:
- , com o conjunto de produções:
- , com o conjunto de produções:
Implementação
As indicações gerais para os exercícios de implementação são válidas aqui.
Uma biblioteca para Grafos Orientados, com Pesquisas
Para este grupo de exercícios pode usar, entre outros, os grafos indicados a seguir.
# Grafos para testar as implementações.
simples = [
(0, 2),
(1, 3),
(2, 0),
(3, 1),
]
cubo = [
(0, 1), (0, 4),
(1, 2), (1, 5),
(2, 3), (2, 6),
(3, 0), (3, 7),
(4, 5), (5, 6), (6, 7), (7, 4)
]
cubo_inclinado = [
(0, 1), (0, 3), (0, 4),
(1, 2), (1, 5),
(2, 6),
(3, 7),
(4, 5), (4, 7),
(5, 6),
(7, 6)
]
- Represente. Uma aresta orientada por um tuplo
(a, b)
. - Represente. Um Grafo (orientado) por uma lista de arestas.
- Implemente. A função
nodes(graph)
que devolve o conjunto do vértices do grafo. - Implemente. A função
children(graph, node)
que devolve o conjunto dos filhos do vérticenode
. - Implemente. A função
parents(graph, node)
que devolve o conjunto dos pais do vérticenode
. - Implemente. A função
is_path(graph, path)
que testa sepath
é um caminho no grafograph
. Um caminho é uma lista de vértices em que dois consecutivos estão ligados por uma aresta. - Implemente. A função
has_loop(graph, path)
que testa sepath
tem algum ciclo no grafograph
. Um ciclo é um caminho que começa e termina no mesmo vértice. - Implemente. A função
search(graph, a, b)
de acordo com os apontamentos teóricos. Considere uma variante em que as funçõesexpand
ejoin
são passadas como argumentos opcionais. Verifique o que acontece se não controlar os vértices visitados. - Teste. Pesquisas ascendentes, descendentes, em largura e em profundidade.
Uma biblioteca para Árvores, com Pesquisas
- Represente. Um nó/vértice é representado pela estrutura
Node
com atributos:value
: Qualquer "coisa" que pode estar no nó.- Obs. nem todas as linguagens permitem isto facilmente. Em
python
ekotlin
, por exemplo, é direto.
- Obs. nem todas as linguagens permitem isto facilmente. Em
children
: Uma lista deNode
.- Obs. esta recursividade também levanta problemas em algumas linguagens. Por exemplo, em
rust
terá de fazer algo do géneroVec<Box<Node>>
. Empython
não terá qualquer dificuldade.
- Obs. esta recursividade também levanta problemas em algumas linguagens. Por exemplo, em
- Implemente. A função
leaf(value)
cria uma folha (isto é, um nó sem filhos). - Implemente. A função
binop(op, lhs, rhs)
cria um nó com valueop
e dois filhos:lhs
erhs
. Considere variantes em quelhs, rhs
sãoNode
ou são um tipo qualquer. - Implemente. A função
node(value, children)
aceita um número variável de valores emchildren
. Por exemplonode("plus", 3, 4, 5)
e nãonode("plus", [3, 4, 5])
. - Escrita. A função
repr(node)
devolve umastring
com a representação de uma árvore, um valor por linha. Indente os descendentes. Por exemplo:
plus
- 3
- times
- 4
- 5
Alternativamente, as árvores podem ser representadas por listas de listas. Por exemplo:
("plus" 3 ("times" 4 5)) =
("plus"
3
("times"
4
5
))
Desta forma, em geral n = Node(value, children)
define a lista [n.value] + CHILDREN
em que CHILDREN
é a lista das representações dos nós em n.children
.
N.B. usando a notação do python, tree.children = tree[1:]
e tree.value= tree[0]
. Além disso, a representação das folhas é o próprio valor, não uma lista.
Pesquisa:
- Implemente. A função
search
para pesquisas (em profundidade e em largura).- Começe com pesquisas por valor exato: Por exemplo,
search(tree, value)
procura emtree
um nón
tal quen.value == value
. No caso de existir um nó nessas condições, devolve o caminho para esse nó.
- Começe com pesquisas por valor exato: Por exemplo,
Caminhos para nós descendentes. Por exemplo, se tree
for o exemplo acima, o caminho para chegar ao nó com o valor 4
é a lista [1, 0]
porque começando na raiz (o nó com valor plus
) escolhe-se o filho na posição 1
(o nó com valor times
e filhos [4, 5]
) e, desse nó escolheu-se o filho na posição 0
; Isto é, o caminho [0, 1]
indicas os indices dos filhos escolhidos, quando se "desce" a árvore.
O caminho para chegar ao valor 5
seria [1, 1]
e para chegar a times
seria apenas [1]
. Finalmente [0]
dá o valor 3
e o caminho []
leva ao valor ... (exercício).
-
Implemente. A função
subtree(tree, path)
devolve o nó que corresponde ao caminhopath
. Se não existe tal nó, devolve "não definido". -
Generalize. Há várias generalizações interessantes:
- Use um predicado em vez de um valor. Por exemplo, para encontrar um nó com o valor par:
tree.searchD(lamba x: x.value % 2 == 0)
. - Encontre todas as ocorrências em vez de apenas uma. Ou encontre até
n
ocorrências. Por exemplo:tree.search(lambda x: len(x.children) > 2, n=42)
para encontrar 42 nós com mais do que dois filhos. Convencione que, sen
for "não definido" procura todas as ocorrências.- A pesquisa de todas as soluções também tem um tratamento interessante que consiste em implementar a pesquisa como uma stream que produz um novo nó sempre que solicitada. No caso do
python
pode usaryield
. Noutras linguagens, ymmv.
- A pesquisa de todas as soluções também tem um tratamento interessante que consiste em implementar a pesquisa como uma stream que produz um novo nó sempre que solicitada. No caso do
- Implemente outras estratégias de pesquisa. Por exemplo a Iterative deepening depth-first, é uma forma melhorada de pesquisas em profundidade.
- Use um predicado em vez de um valor. Por exemplo, para encontrar um nó com o valor par:
Analisadores LL(1)
- Considere a GIC
em que os terminais são e .
Verifique (manualmente) se é LL(1) e, nesse caso, implemente um analisador sintático LL(1). A função
parse(s)
tem como argumento a strings
e devolve uma árvore.
- Comece por separar (tokenize) a palavra numa lista de símbolos.
- Precisa de um "contexto" de processamento com:
- O símbolo seguinte:
next
. - A lista dos símbolos que faltam processar.
- A função
consume(symbol)
.
- O símbolo seguinte:
- Processe a lista de tokens usando as funções
proc_Expr(token, tokens, tree)
eproc_Seq(token, tokens, tree)
:token
é o símbolo de avanço.tokens
são os restantes símbolos da palavras (incluindotoken
).tree
é a árvore que resulta do processamento da função.
- Vai precisar de funções auxiliares como
consume(token, tokens)
Uma biblioteca para GIC LL(1)
Se resolver sozinho estas alíneas ganha o direito de se gabar nas aulas de ALP durante uma semana. E o dever de ajudar os seus colegas.
- ‡ Implemente. O método
firsts()
que devolve um mapa do tiposymbol: set<symbol>
que associa a cada símbolo (variável ou terminal) da gramática o conjunto dos seu primeiros.- ‡ Implemente. O método
follow()
que devolve um mapa do tiposymbol: set<symbol>
que associa a cada símbolo (variável ou terminal) da gramática o conjunto dos seu seguintes.
Análise Sintática LL(1)
- Implemente. O método
firsts_of(symbol)
devolve o conjunto dos primeiros desymbol
. - Implemente. O método
follow_of(symbol)
devolve o conjunto dos seguintes desymbol
. - Implemente. O método
directors(rule)
devolve o conjunto de diretores da regrarule
. - Teste. O método
is_LL1()
testa segrammar
é LL(1).
Uma biblioteca para GIC LR
Itens
- Represente. Um item LR(1) é representado pela classe
Item
com atributos:rule
uma regra.lookahead
uma lista de símbolos.position
um inteiro, que indica a posição do "ponto".
- Teste. O método
is_complete()
testa se o item é completo. - Implemente. O método
active_prefix()
devolve o prefixo até à posição do "ponto". - Implemente. O método
active_symbol()
devolve o símbolo imediatamente a seguir ao ponto se o item não for completo e "não definido" caso contrário. - Implemente. O método
active_suffix()
devolve o sufixo desde a posição a seguir ao "ponto".- N.B:
active_prefix + active_symbol + active_suffix
deve reconstruir o lado direito da produção.
- N.B:
- Implemente. O método
transition(symbol)
devolve o item que resulta de "ler"symbol
. Isto é, sesymbol == active_symbol
no novo item o "ponto" avança uma posição. Caso contrário devolve "não definido". - Escrita. O método
__repr__()
devolve umastring
da formaC〈V → P · A S ⎅ L〉
em que:C
é "'★'
" quando o item é completo e vazia (''
) caso contrário.V
é o lado esquerdo da produção.P
,A
eS
são o prefixo, símbolo e sufixo ativos.L
é a lista dos símbolos de avanço, separados por espaços. Se for vazia, use∅
.- Use mesmo os símbolos indicados: ★〈→·⎅∅〉
Análise Sintática, Autómato dos Itens Válidos
Na biblioteca para os AFD estes são construídos com uma lista de transições, o estado inicial e uma lista de estados finais. Além disso, os estados são inteiros.
Mas, na construção do Autómato dos Itens Válidos os estados "contêm" conjuntos de itens. Esta diferença é resolvida mantendo uma associação (mapa, dicionário) dos estados do AFD para os conjuntos de itens. A vantagem é que não é preciso mudar a implementação dos AFD. A desvantagem é que se torna necessário fazer alguma "manutenção" dessa associação.
-
Representação. Uma classe
VIA
com atributos:grammar
a gramática.fsa
o autómato dos itens válidos calculado paragrammar
.states
a associação dos estados do autómato para os conjuntos de itens.table
a tabela de análise sintática, uma associação dos estados+avanços do autómato para reduções/transferências.phase
um inteiro (ou enum) para marcar a fase de processamento:INITIAL = 0
→GRAMMAR_OK
→START_OK
→VIA_OK
→TABLE_OK
. Se algum destes passos correr mal →ERROR
.
-
Construção. O método
make_VIA()
. Segrammar.is_valid()
incrementaphase
. -
† Implemente. O método
calc_startCore()
que calcula o núcleo do estado inicial devia
. Devolve um conjunto de itens. -
† Implemente. O método
close(itens)
que calcula o fecho de um conjunto de itens. Devolve um conjunto de itens. -
Implemente. O método
build_START()
que calcula o estado inicial devia
. Se correr bem incrementaphase
.
Se resolver sozinho todas as alíneas seguintes ganha o dever de ajudar os seus colegas. E o direito de se gabar nas aulas de ALP durante uma semana.
- † Implemente. O método
build_FSA()
processagrammar
para definir o autómatofsa
, a associaçãostates
eis_built
. Se correr bem incrementaphase
. - † Implemente. O método
build_TABLE()
processagrammar
,fsa
estates
para definirtable
. Se correr bem incrementaphase
.
- † Implemente. O método
derive(word)
para encontrar uma derivação deword
pela gramáticavia.grammar
. N.B. não é necessário implementar autómatos de pilha. - † Implemente. O método
derivation_tree(word)
que calcula a Árvore de Derivação deword
. - Controlo. Use
phase
para evitar computações repetidas, precoces ou desnecessárias. Por exemplo, suponha que houve um erro a calcular o estado inicial. Então quando "chama"build_FSA
deve evitar calcular o AFD. Por outro lado, se já calculoufsa
com sucesso e "acidentalmente" chamabuild_FSA
não há razão para repetir esses cálculos.- Também pode usar
phase
para implementar cálculosbuild---
"inteligentes" (Ah! Ah!). Suponha que "chama"build_TABLE
masphase < VIA_OK
. Nesse ponto pode chamarbuild_VIA
que por sua vez faz uma análise semelhante.
- Também pode usar