Uma introdução à teoria do compilador: análise lexical da linguagem Pascal usando C #

Introdução



Recentemente, a maioria dos novatos em programação começa com linguagens de alto nível, como Java, Python, C # ou qualquer outra linguagem que contenha um "conjunto de cavalheiros" na forma de um coletor de lixo, estruturas de dados prontas e assim por diante. Claro, essa abordagem tem suas vantagens, mas, como regra, um desenvolvedor novato que usa a funcionalidade de linguagem pronta perde o mais importante - sua estrutura e mecanismos de operação e implementação.



Não vou entrar em detalhes sobre alocação de memória e maneiras de interpretar o código, mas pelo contrário, gostaria de falar sobre o próprio compilador, ou seja, o analisador léxico, e tentar implementá-lo em C #. A esmagadora maioria conhece a linguagem que iremos analisar - é Pascal.



O analisador léxico é a primeira das “camadas” do compilador responsável por destacar tokens para processamento posterior.



Um lexema é a menor unidade de um dicionário que representa nossa língua. Palavras de serviço, operadores, identificadores e assim por diante podem servir como um token.



Implementação



Descrição da estrutura



A descrição formal da linguagem será armazenada em duas matrizes: na primeira - palavras de serviço, e na segunda - delimitadores e uma lista com tokens encontrados



private string[] Words = { "program", "var", "integer", "real", "bool", "begin", "end", "if", "then", "else", "while", "do", "read", "write", "true", "false" };
private string[] Delimiter = { ".", ";", ",", "(", ")", "+", "-", "*", "/", "=", ">", "<" };
public List<Lex> Lexemes = new List<Lex>();




O próprio lexema armazenará a chave, que será usada para determinar a associação do tipo (palavras de serviço, operadores, identificadores, números), o id do token e o próprio valor.



class Lex
{
    public int id;
    public int lex;
    public string val;

    public Lex(int _id, int _lex, string _val)
    {
        id = _id;
        lex = _lex;
        val = _val;
    }
}


A melhor solução para processar tokens é uma máquina de estado. Isso eliminará ifs desnecessários e também tornará mais fácil fazer alterações no loop. S é o estado inicial, NUM, DLM, ASGN, ID é o estado dos tipos correspondentes de tokens, ER será usado para erro e FIN para o estado final.



private string buf = ""; //    
private char[] sm = new char[1];
private int dt = 0;
private enum States { S, NUM, DLM, FIN, ID, ER, ASGN, COM } //  state-
private States state; //   
private StringReader sr; //    


Os métodos principais são SearchLex, que procura um token em nosso array e retorna seu id e valor em uma tupla (sim, tuplas também podem ser úteis), e PushLex, que adiciona um novo token ao dicionário.




private (int, string) SerchLex(string[] lexes)
{
    var srh = Array.FindIndex(lexes, s => s.Equals(buf)); 
    if (srh != -1)
        return (srh, buf);             
    else return (-1, "");
}

private (int, string) PushLex(string[] lexes, string buf)
{
    var srh = Array.FindIndex(lexes, s => s.Equals(buf));
    if (srh != -1)
        return (-1, "");
    else
    {
        Array.Resize(ref lexes, lexes.Length + 1);
        lexes[lexes.Length - 1] = buf;
        return (lexes.Length - 1, buf);
    }
}


Implementação de algoritmo



O primeiro passo é determinar o fim do loop - o estado FIN, e também implementar o estado inicial, que será



sr = new StringReader(text); //    
while (state != States.FIN)
{
    switch (state)
    {
        case States.S:
            if (sm[0] == ' ' || sm[0] == '\n' || sm[0] == '\t' || sm[0] == '\0' || sm[0] == '\r' )
                GetNext();
            else if (Char.IsLetter(sm[0]))
            {
                ClearBuf();
                AddBuf(sm[0]);
                state = States.ID;
                GetNext();
            }
            else if (char.IsDigit(sm[0]))
            {
                dt = (int)(sm[0]-'0');
                GetNext();
                state = States.NUM;
                
            }
            else if (sm[0] == '{')
            {
                state = States.COM;
                GetNext();
            }
            else if (sm[0] == ':')
            {
                state = States.ASGN;
                ClearBuf();
                AddBuf(sm[0]);
                GetNext();
            }
            else if (sm[0] == '.')
            {
                AddLex(Lexemes, 2, 0, sm[0].ToString());
                state = States.FIN;
            }
            else
            {
                state = States.DLM;
            }
        break;
    }  
}


O método GetNext permite que você obtenha o próximo caractere na string, ClearBuf, respectivamente, limpa o buffer que armazena o token



private void GetNext()
{
    sr.Read(sm, 0, 1);
}


Deve-se prestar atenção especial ao operador de atribuição ": =", que consiste em dois operadores separados. A maneira mais simples de definir esse operador é adicionar uma condição e gravar o valor intermediário no buffer. Para isso, um estado ASGN separado foi implementado ( na tradução, atribuição - atribuição ). Se o buffer for definido como ":", o algoritmo simplesmente adicionará um novo token e, se o próximo sinal for "=", um operador de atribuição será adicionado.



case States.ASGN:
    if (sm[0] == '=')
    {
        AddBuf(sm[0]);
        AddLex(Lexemes, 2, 4, buf);
        ClearBuf();
        GetNext();
    }
    else
        AddLex(Lexemes, 2, 3, buf);
    state = States.S;
break;


O estado final e o estado com erro são implementados apenas por mensagens de serviço. Você pode melhorar esta opção e verificar o erro também, mas talvez essa funcionalidade possa ser transferida para o analisador.



case States.ER:
    MessageBox.Show("  ");
    state = States.FIN;
    break;
case States.FIN:
    MessageBox.Show("  ");
    break;


Testando



Você pode testar o algoritmo de diferentes maneiras: especifique o caminho do arquivo .pas diretamente , crie programaticamente uma string ou qualquer outra opção conveniente. Como estamos escrevendo em C #, não será difícil adicionar um formulário ao aplicativo, que terá 2 textBoxes, o primeiro para inserir o código do programa, o segundo - exibe o resultado do algoritmo.



Pressionando o botão, iniciaremos a análise do texto, e o resultado resultante será processado usando a construção do switch : além disso, exibiremos o tipo do token encontrado.



private void button1_Click(object sender, EventArgs e)
{
    textBox2.Clear();
    TplMain tpl = new TplMain();
    tpl.Analysis(textBox1.Text);
    
    foreach(var lex in tpl.Lexemes)
    {
        switch (lex.id)
        {
            case 1:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "   "+ Environment.NewLine;
                break;
            case 2:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "  " + Environment.NewLine;
                break;
            case 3:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "  " + Environment.NewLine;
                break;
            case 4:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "  " + Environment.NewLine;
                break;
                
        }     
    }       
}


Dados de entrada



program hellohabr;
	var a, b, c : integer;
begin
	c := a - b + 15;
end.


Resultado



id: 1 lex: 0 val: program |   
id: 4 lex: 1 val: hellohabr |  
id: 2 lex: 1 val: ; |  
id: 1 lex: 1 val: var |   
id: 4 lex: 1 val: a |  
id: 2 lex: 2 val: , |  
id: 4 lex: 1 val: b |  
id: 2 lex: 2 val: , |  
id: 4 lex: 1 val: c |  
id: 2 lex: 3 val: : |  
id: 1 lex: 2 val: integer |   
id: 2 lex: 1 val: ; |  
id: 1 lex: 5 val: begin |   
id: 4 lex: 1 val: c |  
id: 2 lex: 4 val: := |  
id: 4 lex: 1 val: a |  
id: 2 lex: 6 val: - |  
id: 4 lex: 1 val: b |  
id: 2 lex: 5 val: + |  
id: 3 lex: 1 val: 15 |  
id: 2 lex: 1 val: ; |  
id: 1 lex: 6 val: end |   
id: 2 lex: 0 val: . |  


Algoritmo completo



public void Analysis(string text)
{
    sr = new StringReader(text);
    while (state != States.FIN)
    {
        switch (state)
        {

            case States.S:
                if (sm[0] == ' ' || sm[0] == '\n' || sm[0] == '\t' || sm[0] == '\0' || sm[0] == '\r')
                    GetNext();
                else if (Char.IsLetter(sm[0]))
                {
                    ClearBuf();
                    AddBuf(sm[0]);
                    state = States.ID;
                    GetNext();
                }
                else if (char.IsDigit(sm[0]))
                {
                    dt = (int)(sm[0] - '0');
                    GetNext();
                    state = States.NUM;

                }
                else if (sm[0] == '{')
                {
                    state = States.COM;
                    GetNext();
                }
                else if (sm[0] == ':')
                {
                    state = States.ASGN;
                    ClearBuf();
                    AddBuf(sm[0]);
                    GetNext();
                }
                else if (sm[0] == '.')
                {
                    AddLex(Lexemes, 2, 0, sm[0].ToString());
                    state = States.FIN;
                }
                else
                {
                    state = States.DLM;

                }

                break;
            case States.ID:
                if (Char.IsLetterOrDigit(sm[0]))
                {
                    AddBuf(sm[0]);
                    GetNext();
                }
                else
                {
                    var srch = SerchLex(Words);
                    if (srch.Item1 != -1)
                        AddLex(Lexemes, 1, srch.Item1, srch.Item2);
                    else
                    {
                        var j = PushLex(TID, buf);
                        AddLex(Lexemes, 4, j.Item1, j.Item2);
                    }
                    state = States.S;
                }
                break;

            case States.NUM:
                if (Char.IsDigit(sm[0]))
                {
                    dt = dt * 10 + (int)(sm[0] - '0');
                    GetNext();
                }
                else
                {

                    var j = PushLex(TNUM, dt.ToString());
                    AddLex(Lexemes, 3, j.Item1, j.Item2);
                    state = States.S;
                }
                break;
            case States.DLM:
                ClearBuf();
                AddBuf(sm[0]);

                var r = SerchLex(Delimiter);
                if (r.Item1 != -1)
                {
                    AddLex(Lexemes, 2, r.Item1, r.Item2);
                    state = States.S;
                    GetNext();
                }
                else
                    state = States.ER;
                break;
            case States.ASGN:
                if (sm[0] == '=')
                {
                    AddBuf(sm[0]);
                    AddLex(Lexemes, 2, 4, buf);
                    ClearBuf();
                    GetNext();
                }
                else
                    AddLex(Lexemes, 2, 3, buf);
                state = States.S;

                break;
            case States.ER:
                MessageBox.Show("  ");
                state = States.FIN;
                break;
            case States.FIN:
                MessageBox.Show("  ");
                break;
        }
    }
}


Conclusão



Pode parecer que o analisador léxico não é uma coisa muito clara e, na verdade, não é muito importante. Por que você não pode colocar tudo isso em um analisador? Como trabalhar com estruturas complexas? Sim, as maneiras de implementar o analisador léxico diferem de compilador para compilador, mas ao analisar todos esses princípios, você não só entenderá como a linguagem de programação X funciona, mas também terá uma base para desenvolver sua própria linguagem de programação: um segundo Python ou uma linguagem para seu domínio - tudo isso é possível perceber com compreensão de todas as especificidades do trabalho e do dispositivo do compilador em geral.



O projeto pode ser encontrado aqui



All Articles