Tutorial Criando Um Pequeno Compilador
Tutorial Criando Um Pequeno Compilador
Tutorial Criando Um Pequeno Compilador
Neste tutorial será criado, com instruções passo a passo, um pequeno compilador, ou
para ser mais preciso um interpretador, para expressões numéricas.
Neste interpretador serão aceitas espressões com números, as quatro operações básicas e
parênteses. Multiplicação e divisão deverão ter prioridade maior que adição e subtração.
Para este tutorial serão feitos o analisador léxico e o sintático, e os exemplos de código
serão em Java, mas em alguns casos serão também dados exemplos em C++ e em
Delphi para mostrar as diferênças.
Especificação Léxica
A primeira coisa a se fazer em uma especificação léxica e saber quais os tokens que
deverão ser reconhecidos pelo analisador.
Como neste exemplo vão ser precisos números, operadores e parênteses, já é possível
ter uma idéia de quais tokens serão precisos:
• NUMERO
• +
• -
• *
• /
• (
• )
Os tokens são definidos na segunda parte. Nas definições regulares são definidas
expressões auxiliáres, para serem utilizadas na definição dos tokens.
Definições Regulares
D : [0-9]
Tokens
Os tokens para este exemplo são definidos da seguinte forma:
"+"
"-"
"*"
"/"
"("
")"
NUMERO : {D}+
: [\s\t\n\r]*
Primeiro são definidos os operadores. Uma grupo de caracteres entre aspas define um
tokens cuja representação é a de string entre aspas.
Em seguida é definido NUMERO. Para este token é fornecida uma expressão regular
para representá-lo. Nesta expressão é utilizada a definição regular anteriormente
definida. Um NUMERO é um D (digito) repetido uma ou mais vezes. Para utilizar uma
definição deve-se colocá-la entre { e }.
Por fim é descrita uma expressão sem um token associado. Isto indica ao analisador que
ele deve ignorar esta expressão sempre que encontrá-la. Neste caso devem ser ignorados
espaço em branco (\s), tabulação (\t) e quebra de linha (\n e \r).
Expressões Regulares
a reconhece a
ab reconhece a seguido de b
a|b reconhece a ou b
[abc] recohece a, b ou c
[^abc] reconhece qualquer caractere, exceto a, b e c
[a-z] reconhece a, b, c, ... ou z
a* reconhece zero ou mais a's
a+ reconhece um ou mais a's
a? reconhece um a ou nenhum a.
(a|b)* reconhece qualquer número de a's ou b's
. reconhece qualquer caractere, exceto quebra de linha
\123 reconhace o caractere ASCII 123 (decimal)
Os operadores posfixos (*, + e ?) tem prioridade máxima. Em seguida está a
concatenação e por fim a união ( | ). Parenteses podem ser utulisador para agrupar
símbolos e driblar prioridades.
Caracteres especiais
\+ reconhece +
"+*" reconhece + seguido de *
"a""b" reconhece a, seguido de ", seguido de b
\" reconhece "
\n Line Feed
\r Carriage Return
\s Espaço
\t Tabulação
\b Backspace
\e Esc
\XXX O caractere ASCII XXX (XXX é um número decimal)
Pode-se definir ainda um tokens como sendo um caso particular de um outro token. Por
exemplo:
BEGIN = ID : "begin"
END = ID : "end"
WHILE = ID : "while"
Assim define-se que BEGIN, END e WHILE são casos especiais de ID. Sempre que o
analisador encontrar um ID ele procura na lista de casos especiais para ver se este ID
não é um BEGIN ou um WHILE.
Especificação Sintática
Podem ser utilizados na gramática qualquer token já declarado como símbolo terminal.
Os símbolos não-terminais precisam ser previamente declarados em sua área específica.
Esta gramática possui recursões à esquerda e não está fatorada. Não é possível processá-
la com um analisador preditivo sem que antes a gramática seja transformada. Neste
exemplo será feito um analisador SLR, portanto a gramática já está pronta.
As demais ações irão desempilhar dois valores, efetuar uma operação sobre eles e
empilhar o resultado.
Implementação do Semântico
É gerada uma classe para o analisador semântico. Sua implementação porém é por conta
do usuário.
Para este exemplo, o analisador semântico vai precisar apenas de uma pilha para avaliar
as expressões. Em casos mais complexos, como um compilador, será preciso uma tabela
de símbolos também. E o gerador de código deve ser acionado por ações semânticas
também.
import java.util.Stack;
switch (action)
{
case 1:
String tmp = currentToken.getLexeme();
if (tmp.charAt(0) == '0')
throw new SemanticError("Números começados por 0 não são
permitidos", token.getPosition());
stack.push(Integer.valueOf(tmp));
break;
case 2:
b = (Integer) stack.pop();
a = (Integer) stack.pop();
stack.push(new Integer(a.intValue() + b.intValue()));
break;
case 3:
b = (Integer) stack.pop();
a = (Integer) stack.pop();
stack.push(new Integer(a.intValue() - b.intValue()));
break;
case 4:
b = (Integer) stack.pop();
a = (Integer) stack.pop();
stack.push(new Integer(a.intValue() * b.intValue()));
break;
case 5:
b = (Integer) stack.pop();
a = (Integer) stack.pop();
stack.push(new Integer(a.intValue() / b.intValue()));
break;
}
}
}
Foi feita uma restrição semântica para exemplificar como devem ser indicados os erros
semânticos. Se o analisador encontra um erro, ele deve lançar um SemanticError,
passando como parâmetro a mensagem de erro e a posição do erro (que é em geral a
posição do último token).
lexico.setInput( line );
try
{
sintatico.parse(lexico, semantico);
System.out.println(" = ");
System.out.println(trans.getResult());
}
catch ( LexicalError e )
{
e.printStackTrace();
}
catch ( SintaticError e )
{
e.printStackTrace();
}
catch ( SemanticError e )
{
e.printStackTrace();
}
}
}
Para utilizar os analisadores gerados pelo GALS, deve seguir os seguintes passos:
Em Java
Lexico lexico = new Lexico();
Sintatico sintatico = new Sintatico();
Semantico semantico = new Semantico();
...
lexico.setInput( /* entrada */ );
try
{
sintatico.parse(lexico, semantico);
}
catch ( LexicalError e )
{
//Trada erros léxicos
}
catch ( SintaticError e )
{
//Trada erros sintáticos
}
catch ( SemanticError e )
{
//Trada erros semânticos
}
Em C++
Lexico lexico;
Sintatico sintatico;
Semantico semantico;
...
lexico.setInput( /* entrada */ );
try
{
sintatico.parse(&lexico, &semantico);
}
catch ( LexicalError &e )
{
//Trada erros léxicos
}
catch ( SintaticError &e )
{
//Trada erros sintáticos
}
catch ( SemanticError &e )
{
//Trada erros semânticos
}
Em Delphi
lexico : TLexico;
sintatico : TSintatico;
semantico : TSemantico;
...
lexico := TLexico.create;
sintatico := TSintatico.create;
semantico := TSemantico.create;
...
lexico.setInput( /* entrada */ );
try
sintatico.parse(lexico, semantico);
except
on e : ELexicalError do
//Trada erros léxicos
on e : ESintaticError do
//Trada erros sintáticos
on e : ESemanticError do
//Trada erros semânticos
end;
...
lexico.destroy;
sintatico.destroy;
semantico.destroy;
Mesagens de Erro
São geradas mensagens de erro default para os possívies erros. Em alguns casos elas
podem ser apropriadas, mas em geral você vai querer alterá-las para informar ao usuário
uma mensagem mais adequada. As tabelas de erro estão nos arquivos com as
constantes.