📝 BNF: A Notação de Backus-Naur

A Linguagem Universal das Gramáticas

Até este momento, você trabalhou com gramáticas livres de contexto usando notação matemática formal - aquela com símbolos como A \rightarrow \alpha, conjuntos V, \Sigma, e a seta \rightarrow para indicar produções. Esta notação é rigorosa e precisa, perfeita para raciocínio teórico e provas matemáticas. Contudo, quando chegamos à especificação prática de linguagens de programação reais, precisamos de algo ainda mais conveniente e legível.

Entre na BNF (Backus-Naur Form) - uma notação sintática que se tornou o padrão de facto para especificar a sintaxe de linguagens de programação. Criada nos anos 1960 por John Backus e Peter Naur para descrever a linguagem ALGOL 60, BNF revolucionou como especificamos linguagens formais, tornando especificações simultaneamente precisas e acessíveis.

🎯 Por Que BNF é Essencial

BNF não é apenas uma notação alternativa - é a língua franca da especificação de linguagens. Quase toda linguagem de programação moderna tem sua sintaxe documentada em BNF ou alguma variante dela. Dominar BNF significa poder ler especificações oficiais de linguagens, compreender documentação técnica de compiladores, e comunicar designs sintáticos de forma inequívoca com outros desenvolvedores e pesquisadores.

Mais ainda: BNF serve como ponte direta entre especificação teórica e implementação prática. Muitas ferramentas modernas de construção de parsers (como YACC, Bison, ANTLR) aceitam gramáticas escritas em notações baseadas em BNF, transformando especificações declarativas em código funcional automaticamente.


🔤 Anatomia da BNF: Componentes Fundamentais

Estrutura Sintática Básica

BNF é construída sobre três tipos de elementos sintáticos que você precisa reconhecer imediatamente:

📋 Os Três Pilares da BNF

1. Não-Terminais (Variáveis ou Símbolos Não-Terminais)

Não-terminais representam categorias sintáticas que podem ser expandidas em estruturas mais complexas. Em BNF, são tipicamente escritos entre < e >, embora algumas variações usem letras maiúsculas ou palavras capitalizadas.

Convenções comuns:

  • <expressao>, <comando>, <declaracao> (notação clássica)
  • Expressao, Comando, Declaracao (notação sem delimitadores)
  • EXPRESSAO, COMANDO, DECLARACAO (totalmente maiúsculo)

Exemplos de não-terminais típicos em linguagens:

  • <programa> - representa um programa completo
  • <funcao> - representa uma definição de função
  • <if-stmt> - representa um comando condicional
  • <tipo> - representa um tipo de dado

2. Terminais (Símbolos Terminais)

Terminais são os símbolos atômicos que aparecem literalmente nas sentenças da linguagem - os “átomos” que não podem ser decompostos. Em BNF, terminais são tipicamente escritos entre aspas simples '...' ou aspas duplas "...", ou simplesmente sem delimitadores quando não há ambiguidade.

Exemplos de terminais:

  • Palavras-chave: 'if', 'while', 'return', 'func'
  • Operadores: '+', '-', '*', '/', '==', '!='
  • Delimitadores: '(', ')', '{', '}', ';', ','
  • Literais especiais: 'true', 'false', 'null'

3. Produções (Regras de Reescrita)

Produções especificam como não-terminais podem ser expandidos. Em BNF, uma produção tem a forma:

<não-terminal> ::= <lado-direito>

O símbolo ::= (leia-se “é definido como” ou “produz”) é a marca registrada da BNF, distinguindo-a de outras notações. Algumas variações usam : ou ou =, mas ::= é a forma clássica e mais reconhecível.

O lado direito pode conter:

  • Terminais (símbolos literais)
  • Não-terminais (que serão expandidos posteriormente)
  • Sequências de terminais e não-terminais
  • Alternativas (diferentes formas válidas)

O Operador de Alternativa: |

Uma das características mais poderosas da BNF é a capacidade de expressar alternativas compactamente. Quando um não-terminal pode ser expandido de múltiplas formas, usamos o símbolo | (barra vertical, leia-se “ou”) para separar as opções.

🔀 Sintaxe de Alternativas

Formato geral:

<simbolo> ::= <alternativa1> | <alternativa2> | <alternativa3>

Equivalência com múltiplas produções:

Esta linha única é equivalente a escrever três produções separadas:

<simbolo> ::= <alternativa1>
<simbolo> ::= <alternativa2>
<simbolo> ::= <alternativa3>

Exemplo concreto - tipos primitivos:

<tipo> ::= 'int' | 'float' | 'bool' | 'string'

Isso significa: “um <tipo> pode ser int, ou float, ou bool, ou string”.

Exemplo complexo - comandos:

<comando> ::= <atribuicao> 
           | <condicional> 
           | <laco> 
           | <retorno> 
           | <bloco>

Aqui vemos que comandos têm cinco formas diferentes, cada uma sendo um não-terminal que será posteriormente expandido. Observe a formatação em múltiplas linhas para legibilidade - puramente estética, mas altamente recomendada para produções longas.

Insight importante: O operador | tem precedência mais baixa que concatenação. Isso significa que A ::= B C | D E é interpretado como “A produz BC ou DE”, não como “A produz B, depois C ou D, depois E”. Quando houver dúvida, use espaçamento ou quebras de linha para deixar claro onde estão os agrupamentos.


📐 Construindo Gramáticas em BNF: Do Simples ao Complexo

Exemplo Inicial: Expressões Aritméticas Simples

Vamos construir uma gramática BNF passo a passo, começando com algo familiar - expressões aritméticas básicas.

🎓 Exemplo Guiado: Gramática Minimal

Objetivo: Especificar expressões com adição, multiplicação, parênteses e identificadores.

Versão inicial (ingênua):

<expressao> ::= <identificador>
             | <numero>
             | <expressao> '+' <expressao>
             | <expressao> '*' <expressao>
             | '(' <expressao> ')'

<identificador> ::= 'a' | 'b' | 'c' | 'x' | 'y' | 'z'

<numero> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

Análise desta gramática:

Pontos positivos:

  • Simples e intuitiva
  • Captura a ideia básica: expressões podem ser identificadores, números, operações binárias ou expressões parentetizadas

Problemas sérios:

  • Ambiguidade: a + b * c pode ser interpretado como (a + b) * c ou a + (b * c) - ambas são derivações válidas!
  • Precedência não especificada: A gramática não codifica que multiplicação deveria ter precedência sobre adição
  • Associatividade indefinida: a + b + c poderia ser (a + b) + c ou a + (b + c)
  • Recursão à esquerda: Produções como <expressao> ::= <expressao> '+' <expressao> causam problemas para certos tipos de parsers

Este exemplo ilustra um ponto crucial: escrever BNF é fácil; escrever BNF boa é uma arte. A notação em si é simples, mas expressar corretamente as nuances de precedência, associatividade e evitar ambiguidades requer cuidado e experiência.

Refinamento: Incorporando Precedência e Associatividade

Para resolver os problemas da gramática ingênua, precisamos usar uma técnica padrão: hierarquia de não-terminais que reflete hierarquia de precedência.

🔧 Gramática Refinada: Precedência Codificada

Ideia central: Operadores de maior precedência aparecem “mais profundamente” na hierarquia gramatical.

<expressao> ::= <expressao> '+' <termo>
             | <expressao> '-' <termo>
             | <termo>

<termo> ::= <termo> '*' <fator>
         | <termo> '/' <fator>
         | <fator>

<fator> ::= '(' <expressao> ')'
         | <identificador>
         | <numero>

<identificador> ::= 'a' | 'b' | 'c' | 'x' | 'y' | 'z'

<numero> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

Por que isso funciona:

  1. Precedência através de profundidade hierárquica:

    • <expressao> (nível mais alto) contém adição/subtração
    • <termo> (nível intermediário) contém multiplicação/divisão
    • <fator> (nível mais baixo) contém elementos atômicos

    Para derivar a + b * c, começamos com <expressao>. A única forma de introduzir * é através de <termo>, que deve ser completamente resolvido antes de retornar a <expressao> para o +. Isso força b * c a ser agrupado primeiro, respeitando precedência matemática.

  2. Associatividade através de recursão posicionada:

    • <expressao> ::= <expressao> '+' <termo> (recursão à esquerda)

    Esta forma garante associatividade à esquerda para adição. Para derivar a + b + c:

    • Começamos com <expressao>
    • Aplicamos <expressao> '+' <termo> obtendo <expressao> '+' b
    • Aplicamos novamente, obtendo <expressao> '+' <termo> '+' b
    • Finalmente a '+' b '+' c, que corresponde à estrutura ((a + b) + c)
  3. Parênteses como override de precedência:

    • <fator> ::= '(' <expressao> ')'

    Colocar <expressao> completa dentro de <fator> (o nível mais baixo) permite que parênteses forcem qualquer expressão, mesmo com operadores de baixa precedência, a ser tratada como um átomo de alta precedência.

Visualização da derivação: Para a + b * c:

<expressao>
└─ <expressao> '+' <termo>
   └─ <termo> '+' <termo>
      └─ <fator> '+' <termo>
         └─ a '+' <termo>
                  └─ <termo> '*' <fator>
                     └─ <fator> '*' <fator>
                        └─ b '*' c

A estrutura hierárquica da árvore reflete a ordem de avaliação: b * c está em um subárvore completa que deve ser processada antes de retornar ao nível superior para o +.


🏗️ Padrões Comuns em BNF

Listas e Sequências

Um dos padrões mais frequentes em especificações de linguagens é expressar listas de elementos - parâmetros de funções, argumentos de chamadas, declarações de variáveis, comandos em blocos. BNF puro usa recursão para capturar repetição.

📝 Padrão: Lista Separada por Delimitador

Problema: Especificar listas como a, b, c, d (elementos separados por vírgulas).

Solução (recursão à direita):

<lista-ids> ::= <identificador>
             | <identificador> ',' <lista-ids>

Como funciona:

  • Caso base: uma lista pode ser um único identificador
  • Caso recursivo: ou um identificador seguido de vírgula e resto da lista

Derivação de a, b, c:

<lista-ids>
└─ <identificador> ',' <lista-ids>
   └─ a ',' <lista-ids>
            └─ <identificador> ',' <lista-ids>
               └─ b ',' <lista-ids>
                        └─ <identificador>
                           └─ c

Solução alternativa (recursão à esquerda):

<lista-ids> ::= <identificador>
             | <lista-ids> ',' <identificador>

Esta versão constrói a lista da esquerda para direita. Embora matematicamente equivalente, a escolha entre recursão à esquerda e à direita importa para implementação de parsers (você aprenderá mais sobre isso nas próximas semanas).

Padrão: Lista Possivelmente Vazia

📝 Permitindo Listas Vazias

Frequentemente precisamos permitir zero elementos - listas de parâmetros vazias, blocos sem comandos, etc.

Solução 1: Produção épsilon explícita

<lista-comandos> ::= <comando> <lista-comandos>
                  | ε

O símbolo ε (épsilon) representa a cadeia vazia. Esta produção diz: “uma lista de comandos pode ser um comando seguido de mais comandos, ou pode ser vazia”.

Solução 2: Não-terminal auxiliar

<bloco> ::= '{' <comandos> '}'

<comandos> ::= <comando> <comandos>
            | ε

Derivação de bloco vazio {}:

<bloco>
└─ '{' <comandos> '}'
       └─ ε

Derivação de bloco com comandos { a; b; }:

<bloco>
└─ '{' <comandos> '}'
       └─ <comando> <comandos>
          └─ a ';' <comandos>
                   └─ <comando> <comandos>
                      └─ b ';' <comandos>
                               └─ ε

Padrão: Elementos Opcionais

Muitas construções sintáticas têm partes opcionais - cláusulas else em if, tipos de retorno em declarações de funções, inicializadores em declarações de variáveis.

📝 Expressando Opcionalidade

Problema: Comando if com else opcional.

Solução: Produção com épsilon

<if-stmt> ::= 'if' '(' <expressao> ')' <comando> <else-part>

<else-part> ::= 'else' <comando>
             | ε

Análise:

  • Se houver else, a produção <else-part> ::= 'else' <comando> é usada
  • Se não houver else, a produção <else-part> ::= ε é usada (gerando nada)

Exemplo com else: if (x > 0) a = 1; else a = 0;

<if-stmt>
├─ 'if' '(' <expressao> ')' <comando> <else-part>
│                 x > 0          a = 1    └─ 'else' <comando>
│                                               └─ a = 0

Exemplo sem else: if (x > 0) a = 1;

<if-stmt>
├─ 'if' '(' <expressao> ')' <comando> <else-part>
│                 x > 0          a = 1    └─ ε

🎨 Exemplo Completo: Subconjunto de Linguagem Real

Vamos aplicar todos os conceitos aprendidos construindo uma especificação BNF realista para um fragmento substancial de uma linguagem de programação.

🏆 Gramática BNF Completa: Mini-Linguagem

// Estrutura de programa
<programa> ::= <declaracao> <programa>
            | <comando> <programa>
            | ε

// Declarações
<declaracao> ::= <decl-variavel>
              | <decl-funcao>

<decl-variavel> ::= <tipo> <lista-ids> ';'

<lista-ids> ::= <identificador> <init> <mais-ids>

<init> ::= '=' <expressao>
        | ε

<mais-ids> ::= ',' <identificador> <init> <mais-ids>
            | ε

<tipo> ::= 'int'
        | 'float'
        | 'bool'
        | 'string'

<decl-funcao> ::= 'func' <identificador> '(' <parametros> ')' <tipo-retorno> <bloco>

<parametros> ::= <lista-params>
              | ε

<lista-params> ::= <parametro> <mais-params>

<parametro> ::= <tipo> <identificador>

<mais-params> ::= ',' <parametro> <mais-params>
               | ε

<tipo-retorno> ::= ':' <tipo>
                | ε

// Comandos
<comando> ::= <atribuicao>
           | <if-stmt>
           | <while-stmt>
           | <return-stmt>
           | <chamada-funcao> ';'
           | <bloco>

<atribuicao> ::= <identificador> '=' <expressao> ';'

<if-stmt> ::= 'if' '(' <expressao> ')' <comando> <else-clause>

<else-clause> ::= 'else' <comando>
               | ε

<while-stmt> ::= 'while' '(' <expressao> ')' <comando>

<return-stmt> ::= 'return' <expr-opt> ';'

<expr-opt> ::= <expressao>
            | ε

<bloco> ::= '{' <lista-comandos> '}'

<lista-comandos> ::= <comando> <lista-comandos>
                  | ε

// Expressões (com precedência)
<expressao> ::= <expr-or>

<expr-or> ::= <expr-or> '||' <expr-and>
           | <expr-and>

<expr-and> ::= <expr-and> '&&' <expr-eq>
            | <expr-eq>

<expr-eq> ::= <expr-eq> '==' <expr-rel>
           | <expr-eq> '!=' <expr-rel>
           | <expr-rel>

<expr-rel> ::= <expr-rel> '<' <expr-add>
            | <expr-rel> '<=' <expr-add>
            | <expr-rel> '>' <expr-add>
            | <expr-rel> '>=' <expr-add>
            | <expr-add>

<expr-add> ::= <expr-add> '+' <expr-mult>
            | <expr-add> '-' <expr-mult>
            | <expr-mult>

<expr-mult> ::= <expr-mult> '*' <expr-unary>
             | <expr-mult> '/' <expr-unary>
             | <expr-mult> '%' <expr-unary>
             | <expr-unary>

<expr-unary> ::= '-' <expr-unary>
              | '!' <expr-unary>
              | <expr-primary>

<expr-primary> ::= <identificador>
                | <numero>
                | <string-literal>
                | <boolean>
                | <chamada-funcao>
                | '(' <expressao> ')'

<chamada-funcao> ::= <identificador> '(' <argumentos> ')'

<argumentos> ::= <lista-args>
              | ε

<lista-args> ::= <expressao> <mais-args>

<mais-args> ::= ',' <expressao> <mais-args>
             | ε

// Terminais léxicos
<identificador> ::= <letra> <resto-id>

<resto-id> ::= <letra> <resto-id>
            | <digito> <resto-id>
            | '_' <resto-id>
            | ε

<numero> ::= <digitos> <parte-decimal>

<digitos> ::= <digito> <digitos>
           | <digito>

<parte-decimal> ::= '.' <digitos>
                 | ε

<string-literal> ::= '"' <chars> '"'

<chars> ::= <char> <chars>
         | ε

<boolean> ::= 'true'
           | 'false'

<letra> ::= 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j'
         | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't'
         | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
         | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J'
         | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T'
         | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'

<digito> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

Características desta gramática:

  1. Hierarquia completa de expressões:
    • Sete níveis de precedência (OR → AND → igualdade → relacional → aditivo → multiplicativo → unário → primário)
    • Cada operador no seu nível apropriado
  2. Estruturas opcionais bem definidas:
    • Parâmetros de funções podem ser vazios
    • Tipo de retorno de funções é opcional
    • Cláusula else é opcional
    • Inicializadores de variáveis são opcionais
  3. Listas com padrões consistentes:
    • Parâmetros, argumentos, variáveis - todos seguem o mesmo padrão recursivo
  4. Separação clara entre níveis:
    • Nível de programa (declarações e comandos)
    • Nível de declarações (variáveis e funções)
    • Nível de comandos (atribuições, controle, etc.)
    • Nível de expressões (hierarquia de precedência)
    • Nível léxico (identificadores, números, strings)

🔬 Análise Crítica: Limitações e Problemas Comuns da BNF

Problemas que BNF Não Resolve Automaticamente

Embora BNF seja poderosa, ela não é mágica. Há vários problemas que especificações BNF podem ter, e detectá-los requer análise cuidadosa.

⚠️ Armadilhas Comuns em BNF

1. Ambiguidade

BNF pode facilmente especificar gramáticas ambíguas - onde uma mesma sentença tem múltiplas árvores de derivação válidas.

Exemplo clássico: dangling else

<comando> ::= <if-simples>
           | <if-com-else>
           | <outro-comando>

<if-simples> ::= 'if' '(' <expr> ')' <comando>

<if-com-else> ::= 'if' '(' <expr> ')' <comando> 'else' <comando>

Para if (a) if (b) x = 1; else x = 2;, há duas interpretações:

  • O else pertence ao primeiro if?
  • Ou ao segundo if?

Ambas são derivações válidas! A gramática é ambígua.

Solução: Reescrever para forçar uma interpretação (convencionalmente, else pertence ao if mais próximo):

<comando> ::= <comando-casado>
           | <comando-aberto>

<comando-casado> ::= 'if' '(' <expr> ')' <comando-casado> 'else' <comando-casado>
                  | <outro-comando>

<comando-aberto> ::= 'if' '(' <expr> ')' <comando>
                  | 'if' '(' <expr> ')' <comando-casado> 'else' <comando-aberto>

2. Recursão à Esquerda

Produções como <A> ::= <A> α | β causam problemas para parsers descendentes recursivos - eles entram em loop infinito.

Exemplo problemático:

<expressao> ::= <expressao> '+' <termo>
             | <termo>

Por que é problema: Um parser descendente tentando reconhecer <expressao> imediatamente tenta reconhecer <expressao> novamente, antes de consumir qualquer entrada - loop infinito!

Solução: Eliminar recursão à esquerda transformando em recursão à direita com épsilon:

<expressao> ::= <termo> <expressao'>

<expressao'> ::= '+' <termo> <expressao'>
              | ε

3. Verbosidade e Redundância

BNF pura pode ser extremamente verbosa para padrões comuns, levando a especificações longas e difíceis de manter.

Exemplo: Para especificar que algo pode se repetir zero ou mais vezes, precisamos de recursão explícita:

<lista-declaracoes> ::= <declaracao> <lista-declaracoes>
                     | ε

Isso funciona, mas é prolixo. Linguagens com muitas estruturas repetitivas resultam em gramáticas enormes com dezenas de não-terminais auxiliares que existem apenas para expressar repetição ou opcionalidade.

Quando BNF Não é Suficiente

🔍 Limitações Inerentes

BNF (sendo notação para gramáticas livres de contexto) não pode expressar todas as restrições sintáticas de linguagens de programação reais. Algumas restrições são sensíveis ao contexto ou até semânticas, não puramente sintáticas:

  • Declaração antes de uso: “Uma variável deve ser declarada antes de ser usada” - BNF não consegue expressar esta restrição
  • Compatibilidade de tipos: “Operandos de + devem ter tipos compatíveis” - análise semântica, não sintática
  • Unicidade de identificadores: “Nenhum identificador pode ser declarado duas vezes no mesmo escopo” - requer análise de tabela de símbolos
  • Correspondência de parênteses balanceados com números específicos: Embora GLC possa expressar \{a^n b^n\}, restrições mais complexas como “o número de declarações deve ser igual ao número de usos” estão além de GLC

Para estas restrições, usamos análise semântica após parsing sintático. BNF especifica a estrutura superficial; semântica valida propriedades mais profundas.


🛠️ BNF na Prática: Ferramentas e Aplicações

Geradores de Parsers Baseados em BNF

Uma das razões pelas quais BNF é tão importante é que ela serve como linguagem de entrada para geradores automáticos de parsers. Estas ferramentas transformam especificações declarativas em código funcional.

🔧 Ferramentas Populares

YACC (Yet Another Compiler Compiler) e seu sucessor moderno Bison:

  • Aceita gramáticas em notação BNF estendida
  • Gera parsers ascendentes (shift-reduce) em C/C++
  • Usado em projetos como GCC, PHP, Ruby

Sintaxe típica (YACC/Bison):

expressao: expressao '+' termo  { $$ = $1 + $3; }
         | termo                { $$ = $1; }
         ;

termo: termo '*' fator          { $$ = $1 * $3; }
     | fator                    { $$ = $1; }
     ;

fator: '(' expressao ')'        { $$ = $2; }
     | NUMERO                   { $$ = $1; }
     ;

Observe a similaridade com BNF puro, mas com ações semânticas em { }.

ANTLR (ANother Tool for Language Recognition):

  • Aceita gramáticas em notação próxima a EBNF
  • Gera parsers descendentes (LL(*)) em Java, C#, Python, C++, JavaScript
  • Usado em ferramentas como Twitter Search, NetBeans, Hibernate

Sintaxe típica (ANTLR):

expressao
    : termo ('+' termo)*
    ;

termo
    : fator ('*' fator)*
    ;

fator
    : '(' expressao ')'
    | NUMERO
    ;

Note o uso de * para repetição - isso é EBNF, não BNF puro, mas baseado nos mesmos princípios.

Especificações de Linguagens Reais

Quase toda linguagem de programação documenta sua sintaxe usando BNF ou variantes:

Linguagem Notação Usada Exemplo de Regra
C BNF tradicional <selection-statement> ::= if ( <expression> ) <statement>
Java BNF estendida IfThenStatement: if ( Expression ) Statement
Python EBNF modificada if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
JavaScript (ECMAScript) Variante BNF própria IfStatement : if ( Expression ) Statement else Statement
Rust Notação informal baseada em BNF IfExpression : if Expression BlockExpression

💼 Aplicando BNF no Seu Projeto Integrador

Especificando a Gramática

No Projeto Integrador, uma das primeiras tarefas será especificar formalmente a sintaxe da linguagem. BNF será sua ferramenta principal para isso.

🎯 Passos Recomendados

1. Comece com estrutura de alto nível:

<programa> ::= <item-programa> <programa>
            | ε

<item-programa> ::= <declaracao>
                 | <comando>

2. Expanda declarações:

<declaracao> ::= <decl-variavel>
              | <decl-funcao>
              | <decl-tipo>

3. Especifique comandos:

<comando> ::= <atribuicao>
           | <condicional>
           | <laco>
           | <retorno>
           | <bloco>

4. Desenvolva hierarquia de expressões:

Comece com o nível mais externo (menor precedência) e desça até o mais interno (maior precedência).

5. Defina terminais léxicos:

<identificador> ::= <letra> <resto-id>
<resto-id> ::= <letra> <resto-id>
            | <digito> <resto-id>
            | '_' <resto-id>
            | ε

6. Valide com exemplos:

Para cada regra, escreva alguns programas exemplo e derive-os manualmente para verificar se a gramática produz o que você espera.