Programação de Autômatos Finitos Determinísticos

Centro Federal de Educacao Tecnologica de Minas Gerais

Alain André Tomaz Amaral1 Thiago Mendes Vieira2

RESUMO Este trabalho propõe um material de apoio para estudo de implementação de autômatos na programação. Tem como objetivo mostrar métodos, técnicas e exemplos de implementação de autômatos finitos determinísticos na programação, bem como apresentar a ferramenta Lex, uma ferramenta de apoio que permite gerar código verificador de linguagens regulares através de definições de suas expressões regulares. ABSTRACT This paper proposes a support material for the study of automata implementation on programming. Aims to show the methods, techniques and examples of implementation of deterministic finite automata programming, as well as presenting the tool Lex, a support tool to generate code checker regular languages via settings of your regular expressions.

Palavras-chaves: autômato, programação, expressão regular, Lex

1

Graduando em Engenharia de Computação (CEFET-MG). Email: alainandreamaral@gmail.com 2 Graduando em Engenharia de Computação (CEFET-MG). Email: thiagomv.0301@gmail.com

1 INTRODUÇÃO Os conceitos de autômatos constituem um modelo útil para muitos elementos importantes de hardware e software. Alguns dos elementos mais importantes onde tais conceitos são empregados são: 1. Software que verifica o comportamento de circuitos digitais; 2. O “analisador léxico” de um compilador típico; 3. Software que encontra ocorrências de palavras, frases ou padrões em textos; 4. Software para verificar sistemas que têm um número finito de estados distintos, como protocolos. (HOPCROFT et al., 2002) O reconhecimento de linguagens é uma das principais aplicações das teorias de autômatos. No estudo aqui apresentado serão mostradas técnicas e ferramentas utilizadas para implementação de softwares reconhecedores de linguagens. As linguagens que serão abordadas no estudo serão as Linguagens Regulares.

2 SOFTWARE DE RECONHECIMENTO DE LINGUAGENS REGULARES 2.1 Estudo de caso: Reconhecimento de dadas válidas 2.1.1 O diagrama de transição Vamos iniciar nossos estudos de autômatos na programação apresentando um diagrama de transição que reconhece uma data válida no formato aaaa/mm/dd. Para simplificação do problema vamos desconsiderar os casos de anos bissextos.

Figura 1 - Autômato reconhecedor de data válida

Como se pode ver este autômato possui três estados finais, que identificam a quantidade de dias do mês, além de verificar a validade da data.

Existem várias formas de usar uma coleção de diagramas de transição para construir um algoritmo verificador de linguagem. Podemos imaginar uma variável state contendo o número do estado corrente para o diagrama de transição. Um comando switch baseado no valor de state que nos leva ao código para cada um dos possíveis estados, onde encontramos a ação desse estado. Normalmente o código para um estado é ele mesmo, um comando switch ou um desvio de múltiplos caminhos que determina o próximo estado, lendo e examinando o próximo caractere da entrada. 2.1.2 O algoritmo Abaixo o algoritmo verificador de data escrito na linguagem c++:
#include <iostream> using namespace std; #define #define #define #define #define STATE_ERROR 1000 D28 1 D30 2 D31 3 R_ERROR -1

int main(){ int state = 0; //guarda o estado atual no automato char str[1000];//buffer para armazenar a palavra a ser verificada cin.getline(str,1000); int contadorAno = 0; //conta os dígitos do ano int index = -1; //percorre cada posição no buffer char now; //caracter atual que esta sendo analizado bool running = true; int result; //resultado encontrado na verificação da palavra while(running){ index++;//próxima posição no buffer now=str[index];//símbolo atual na palavra switch(state){ case 0: if(isdigit(now)){ contadorAno++; if(contadorAno==4) state = 1; }else{ state = STATE_ERROR; } break; case 1: if(now=='/') state = 2; else state = STATE_ERROR;

case 2:

break; if(now=='0') state = 3; else if(now=='1') state = 4; else state = STATE_ERROR; break; if(now=='1' || now=='3' || now=='5' || now=='7' || now=='8') state = 5; else if(now=='4' || now=='6' || now=='9') state = 6; else if(now=='2') state = 7; else state = STATE_ERROR; break; if(now=='0' || now=='2') state = 8; else if(now=='1') state = 9; else state = STATE_ERROR; break; if(now=='/') state = 10; else state = STATE_ERROR; break; if(now=='/') state = 11; else state = STATE_ERROR; break; if(now=='/') state = 12; else state = STATE_ERROR; break; if(now=='/') state = 10; else state = STATE_ERROR; break;

case 3:

case 4:

case 5:

case 6:

case 7:

case 8:

case 9:

if(now=='/') state = 11; else state = STATE_ERROR; break; case 10:

case

case

case

case

case

case

case

if (now == '0') state = 13; else if (now == '1'||now == '2') state = 14; else if (now == '3') state = 15; else state = STATE_ERROR; break; 11: if (now == '3') state = 16; else if (now == '1'||now == '2') state = 17; else if (now == '0') state = 18; else state = STATE_ERROR; break; 12: if (now == '2') state = 19; else if (now == '0') state = 20; else if (now == '1') state = 21; else state = STATE_ERROR; break; 13: if (isdigit(now) && now != '0') state = 22; else state = STATE_ERROR; break; 14: if (isdigit(now)) state = 22; else state = STATE_ERROR; break; 15: if ( now == '0' || now == '1') state = 22; else state = STATE_ERROR; break; 16: if (now == '0') state = 23; else state = STATE_ERROR; break; 17: if (isdigit(now)) state = 23; else state = STATE_ERROR;

}

break; case 18: if (isdigit(now) && now != '0') state = 23; else state = STATE_ERROR; break; case 19: if (isdigit(now) && now != '9') state = 24; else state = STATE_ERROR; break; case 20: if (isdigit(now) && now != '0') state = 24; else state = STATE_ERROR; break; case 21: if (isdigit(now)) state = 24; else state = STATE_ERROR; break; case 22: result=D31; if(index+1==strlen(str)) state = STATE_ERROR; else running = false; break; case 23: result=D30; if(index+1==strlen(str)) state = STATE_ERROR; else running = false; break; case 24: result=D28; if(index+1==strlen(str)) state = STATE_ERROR; else running = false; break; case STATE_ERROR: result=R_ERROR; running = false; break; }

//Analiza o resultado encontrado switch(result){ case R_ERROR: cout<<"ERRO\n"; break;

case D28: cout<<"Mes com 28 dias\n"; break; case D30: cout<<"Mes com 30 dias\n"; break; case D31: cout<<"Mes com 31 dias\n"; break; } }

O algoritmo acima é baseado no diagrama de transição do autômato verificador de datas válidas (figura 1). Ele faz a leitura de uma palavra e em seguida verifica se esta é uma data válida. Na verificação é retornado um resultado que identifica a quantidade de dias do mês da data, ou um erro, caso a palavra não for uma data válida. 2.2 A ferramenta Lex Em ciência da computação (linguagens de programação), lex é um programa que gera analisadores léxicos. Ele é geralmente usado com o yacc, um gerador de analisador sintático. Escrito originalmente por Eric Schmidt e Mike Lesk, ele é o gerador de analisador léxico padrão em diversos sistemas Unix. O lex lê um fluxo de entrada especificando um analisador que mapeia expressões regulares em blocos de código, e retorna um código fonte implementando o analisador. Apesar do gerador ser genérico e poder se adequar a diferentes linguagens de programação, atualmente, somente a geração de código C é suportada. Apesar de ser software proprietário, versões do lex baseadas no código original da AT&T estão disponíveis em código aberto, como parte de sistemas como OpenSolaris e Plan 9. Outra versão popular e livre do lex é o flex. A estrutura de um arquivo lex é intencionalmente similar ao de um arquivo yacc. Os arquivos são divididos em três seções, separadas por linhas que contém somente dois símbolos de porcentagem, como a seguir: definições %% regras %% subrotinas

Na seção de definições são definidas as macros e são importadas as bibliotecas escritas em C. É também possível escrever código C na mesma seção. Já a seção de regras associa padrões com instruções C, padrões escritos na forma de expressões regulares. Quando o analisador léxico identifica algum texto da entrada casando com um padrão, ele executa o código C associado. A tentativa do casamento é sempre gananciosa, isto é, no caso de dois padrões distintos casando a mesma entrada, o maior deles será usado. O maior deles é o que consome mais caracteres da entrada. Caso os padrões ambíguos consumam a mesma quantidade de caracteres, o padrão definido antes é escolhido. Por fim, a seção de subrotinas contém blocos de código C que serão apenas copiados ao arquivo final. Assume-se que tal código será invocado a partir das regras da seção de regras. Em programas maiores, é mais conveniente separar esse código final noutro arquivo. 2.2.1 Implementando o autômato verificador de datas na ferramenta Lex Para implementar o autômato devemos primeiro criar a expressão regular. Usaremos o seguinte padrão para implementar nosso autômato na ferramenta Lex:
%{ #include <iostream> %} %% <expressão regular> printf(<mensagem>); <expressão regular> printf(<mensagem>); <expressão regular> printf(<mensagem>); <...> %%

Resumo das expressões regulares: . => qualquer caractere em sua posição

[acz] => qualquer um dos caracteres, no caso: a, c ou z [a-d] => qualquer caracter do intervalo, no caso: a,b,c ou d [157] => 1,5 ou 7 [0-7] => 0,1,2,3,4,5,6 ou 7

[^Z] => qualquer caractere, exceto Z a* => "a" zero ou mais vezes a+ => "a" uma ou mais vezes a? => "a" zero ou uma vez a{5} => "a" cinco vezes a{5,} => "a" de cinco a infinito vezes a{,5} => "a" de zero a cinco vezes ^a => "a" no início da linha a$ => "a" no final da linha .* => qualquer caractere Observações:

As expressões regulares podem ser agrupadas formando outras expressões regulares mais complexas Não esqueça que o curinga conhecido ( * ), em expressões regulares, não significa qualquer caracter e sim o caracter anterior zero ou mais vezes. Em expressões regulares qualquer caracter é representado pela expressão .*

A expressão regular pode ser dividida em várias partes, pois a união de várias linguagens regulares é uma linguagem regular. O código final para o Lex será:
%{ #include <stdio.h> %} %% [0-9][0-9][0-9][0-9][/][0][2][/]([0][1-9]|[1][0-9]|[2][0-8]) printf("Mes com 28 dias\n"); [0-9][0-9][0-9][0-9][/]([0][13578]|[1][02])[/]([0][1-9]|[1-2][0-9]|[3][0-1]) printf("Mes com 31 dias\n"); [0-9][0-9][0-9][0-9][/]([0][469]|[1][1])[/]([0][1-9]|[1-2][0-9]|[3][0]) printf("Mes com 30 dias\n"); .* printf("Data Invalida\n"); %%

3 REFERÊNCIAS AHO, A. V.; SETHI, R.; ULLMAN, J. D. Compiladores: princípios, técnicas e ferramentas. São Paulo: Pearson Addison-Wesley,2008. 634p. ISBN 9788588639249 DIAS NETO, Samuel. Expressões Regulares. Disponível em: <http://homepages.dcc.ufmg.br/~joaoreis/Site%20de%20tutoriais/tools/er.html>. Acesso em: 10 dez. 2011. HOPCROFT, John E.; ULLMAN, Jeffrey D.; MOTWANI, Rajeev. Introdução àteoria de autômatos, linguagens e computação. Rio de Janeiro: Elsevier,2002. 560 p. ISBN 8535210725 HUBERT, Bert. Lex e YACC primer. Disponível em: <http://lexyacc.codigolivre.org.br>. Acesso em: 10 dez. 2011.