📚 Definição Formal de Gramáticas Livres de Contexto
A Estrutura Fundamental
Uma gramática livre de contexto é um sistema de reescrita que especifica como construir palavras válidas de uma linguagem através de regras de produção hierárquicas.
📋 Definição Formal
Uma gramática livre de contexto é uma 4-tupla G = (V, \Sigma, P, S) onde:
- V é um conjunto finito de variáveis (símbolos não-terminais)
- \Sigma é um conjunto finito de símbolos terminais (alfabeto da linguagem)
- P é um conjunto finito de produções da forma A \rightarrow \alpha
- S \in V é a variável inicial (símbolo inicial)
Restrições importantes:
- V \cap \Sigma = \varnothing (terminais e não-terminais são disjuntos)
- Em cada produção A \rightarrow \alpha: A \in V e \alpha \in (V \cup \Sigma)^*
- A diferença das gramáticas regulares: o lado direito \alpha pode conter qualquer combinação de terminais e não-terminais
Esta definição pode parecer similar às gramáticas regulares que você estudou anteriormente, mas a diferença fundamental está na forma das produções. Enquanto gramáticas regulares restringem severamente a forma do lado direito das produções, gramáticas livres de contexto permitem flexibilidade total, possibilitando a expressão de estruturas recursivas complexas.
O que torna essas gramáticas “livres de contexto” é a forma restrita das produções. Cada produção tem exatamente um não-terminal do lado esquerdo, independentemente do contexto em que esse não-terminal aparece. Esta propriedade simplifica enormemente o processamento, mas ainda permite expressar estruturas recursivas complexas que são essenciais para linguagens de programação.
Componentes em Detalhes
Variáveis (Não-terminais): Representam categorias sintáticas ou conceitos que podem ser expandidos em estruturas mais complexas. Em gramáticas para linguagens de programação, variáveis típicas incluem \text{Expressao}, \text{Comando}, \text{Programa}, \text{Declaracao}. Cada variável representa uma classe de estruturas que compartilham propriedades sintáticas comuns.
Símbolos Terminais: São os elementos atômicos da linguagem - tokens que aparecem diretamente nas palavras finais. Para linguagens de programação, incluem palavras-chave como \text{if}, \text{while}, operadores como +, *, e símbolos especiais como parênteses e ponto-e-vírgula.
Produções: Especificam como variáveis podem ser expandidas ou “reescritas” em combinações de outras variáveis e terminais. A produção A \rightarrow \alpha significa “a variável A pode ser substituída pela string \alpha”. Esta substituição é o mecanismo fundamental através do qual derivações constroem palavras da linguagem.
Símbolo Inicial: A variável de onde todas as derivações começam. Tipicamente representa o conceito mais abrangente da linguagem - para linguagens de programação, frequentemente algo como \text{Programa} ou \text{UnidadeDeCompilacao}.
Exemplo Detalhado: Expressões Aritméticas
Para ilustrar estes conceitos, consideremos uma gramática para expressões aritméticas simples:
// Definição da gramática para expressões aritméticas
class GramaticaExpressoes {
// V = {E, T, F} - Variáveis (não-terminais)
static const Set<String> variaveis = {'E', 'T', 'F'};
// Σ = {+, *, (, ), id} - Símbolos terminais
static const Set<String> terminais = {'+', '*', '(', ')', 'id'};
// S = E - Símbolo inicial
static const String simboloInicial = 'E';
// P - Conjunto de produções
static const Map<String, List<List<String>>> producoes = {
'E': [
['E', '+', 'T'], // E → E + T
['T'] // E → T
],
'T': [
['T', '*', 'F'], // T → T * F
['F'] // T → F
],
'F': [
['(', 'E', ')'], // F → ( E )
['id'] // F → id
]
};
// Método para derivar uma expressão passo a passo
static List<String> derivarExpressao(String expressao) {
List<String> passos = [];
passos.add('Início: E');
// Exemplo de derivação para "id + id * id"
if (expressao == 'id + id * id') {
passos.add('E → E + T');
passos.add('E + T → T + T');
passos.add('T + T → F + T');
passos.add('F + T → id + T');
passos.add('id + T → id + T * F');
passos.add('id + T * F → id + F * F');
passos.add('id + F * F → id + id * F');
passos.add('id + id * F → id + id * id');
}
return passos;
}
// Visualização da árvore de derivação
static String gerarArvore(String expressao) {
// Retorna representação textual da árvore
return '''
E
├── E
│ └── T
│ └── F
│ └── id
├── +
└── T
├── T
│ └── F
│ └── id
├── *
└── F
└── id
''';
}
}#include <string>
#include <vector>
#include <set>
#include <map>
class GramaticaExpressoes {
public:
// V = {E, T, F} - Variáveis (não-terminais)
static const std::set<std::string> variaveis;
// Σ = {+, *, (, ), id} - Símbolos terminais
static const std::set<std::string> terminais;
// S = E - Símbolo inicial
static const std::string simboloInicial;
// P - Conjunto de produções
static const std::map<std::string, std::vector<std::vector<std::string>>> producoes;
// Deriva uma expressão passo a passo
static std::vector<std::string> derivarExpressao(const std::string& expressao);
// Gera representação textual da árvore
static std::string gerarArvore(const std::string& expressao);
};
const std::set<std::string> GramaticaExpressoes::variaveis = {"E", "T", "F"};
const std::set<std::string> GramaticaExpressoes::terminais = {"+", "*", "(", ")", "id"};
const std::string GramaticaExpressoes::simboloInicial = "E";
const std::map<std::string, std::vector<std::vector<std::string>>>
GramaticaExpressoes::producoes = {
{"E", {{"E", "+", "T"}, {"T"}}},
{"T", {{"T", "*", "F"}, {"F"}}},
{"F", {{"(", "E", ")"}, {"id"}}}
};
std::vector<std::string> GramaticaExpressoes::derivarExpressao(
const std::string& expressao) {
std::vector<std::string> passos;
passos.push_back("Início: E");
// Exemplo de derivação para "id + id * id"
if (expressao == "id + id * id") {
passos.push_back("E → E + T");
passos.push_back("E + T → T + T");
passos.push_back("T + T → F + T");
passos.push_back("F + T → id + T");
passos.push_back("id + T → id + T * F");
passos.push_back("id + T * F → id + F * F");
passos.push_back("id + F * F → id + id * F");
passos.push_back("id + id * F → id + id * id");
}
return passos;
}Esta gramática ilustra elegantemente como a estrutura hierárquica codifica precedência de operadores. Observe que multiplicação aparece “mais profundamente” na hierarquia (em T e F) do que adição (que aparece no nível mais alto, em E). Esta profundidade estrutural força a multiplicação a ser avaliada antes da adição, implementando as regras matemáticas de precedência que aprendemos desde a escola.