Neste artigo, vamos aprender como criar sua própria linguagem de programação usando SLY (Sly Lex Yacc) e Python. Antes de nos aprofundarmos neste tópico, deve-se notar que este não é um tutorial para iniciantes e você precisa ter algum conhecimento dos pré-requisitos fornecidos abaixo.

Pré-requisitos

  • Conhecimento aproximado sobre design de compiladores.
  • Compreensão básica da análise lexical, análise sintática e outros aspectos do projeto do compilador.
  • Compreensão de expressões regulares.
  • Familiaridade com a linguagem de programação Python.

Começando

Instale SLY para Python. SLY é uma ferramenta de lexing e análise que torna nosso processo muito mais fácil.

pip install sly

Construindo um Lexer

A primeira fase de um compilador é converter todos os fluxos de caracteres (o programa de alto nível que é escrito) em fluxos de token. Isso é feito por um processo chamado análise lexical. No entanto, este processo é simplificado usando SLY

Primeiro, vamos importar todos os módulos necessários.

from sly import Lexer

Agora vamos construir uma classe BasicLexer que estende a classe Lexer de SLY. Vamos fazer um compilador que faça operações aritméticas simples. Portanto, precisaremos de alguns tokens básicos, como NAME , NUMBER , STRING . Em qualquer linguagem de programação, haverá espaço entre dois caracteres. Assim, criamos um literal ignorar . Em seguida, também criamos os literais básicos como '=', '+' etc., os tokens NAME são basicamente nomes de variáveis, que podem ser definidos pela expressão regular [a-zA-Z _] [a-zA-Z0-9_] *. Os tokens STRING são valores de string e são delimitados por aspas (”“). Isso pode ser definido pela expressão regular \ ”. *? \”.

Sempre que encontrarmos dígito / s, devemos alocá-lo ao token NUMBER e o número deve ser armazenado como um inteiro. Estamos fazendo um script programável básico, então vamos apenas fazer com inteiros, no entanto, fique à vontade para estender o mesmo para decimais, longos etc., Também podemos fazer comentários. Sempre que encontramos “//”, ignoramos tudo o que vier a seguir nessa linha. Fazemos a mesma coisa com o caractere de nova linha. Assim, construímos um lexer básico que converte o fluxo de caracteres em fluxo de tokens.

class BasicLexer(Lexer): 
    tokens = { NAME, NUMBER, STRING } 
    ignore = '\t '
    literals = { '=', '+', '-', '/'
                '*', '(', ')', ',', ';'} 
  
  
    
    
    NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'
    STRING = r'\".*?\"'
  
    
    @_(r'\d+') 
    def NUMBER(self, t): 
      
        t.value = int(t.value)  
        return t 
  
    
    @_(r'//.*') 
    def COMMENT(self, t): 
        pass
  
    
    
    @_(r'\n+') 
    def newline(self, t): 
        self.lineno = t.value.count('\n')

Construindo um analisador

 Primeiro, vamos importar todos os módulos necessários.

from sly import Parser

Agora vamos construir uma classe BasicParser que estende a classe Lexer . O fluxo de tokens do BasicLexer é passado para tokens variáveis . A precedência é definida, que é a mesma para a maioria das linguagens de programação. A maior parte da análise escrita no programa abaixo é muito simples. Quando não há nada, a afirmação não passa nada. Basicamente, você pode pressionar Enter no teclado (sem digitar nada) e ir para a próxima linha. Em seguida, seu idioma deve compreender as atribuições usando o “=”. Isso é tratado na linha 18 do programa abaixo. A mesma coisa pode ser feita quando atribuída a uma string.

class BasicParser(Parser): 
    
    tokens = BasicLexer.tokens 
  
    precedence = ( 
        ('left', '+', '-'), 
        ('left', '*', '/'), 
        ('right', 'UMINUS'), 
    ) 
  
    def __init__(self): 
        self.env = { } 
  
    @_('') 
    def statement(self, p): 
        pass
  
    @_('var_assign') 
    def statement(self, p): 
        return p.var_assign 
  
    @_('NAME "=" expr') 
    def var_assign(self, p): 
        return ('var_assign', p.NAME, p.expr) 
  
    @_('NAME "=" STRING') 
    def var_assign(self, p): 
        return ('var_assign', p.NAME, p.STRING) 
  
    @_('expr') 
    def statement(self, p): 
        return (p.expr) 
  
    @_('expr "+" expr') 
    def expr(self, p): 
        return ('add', p.expr0, p.expr1) 
  
    @_('expr "-" expr') 
    def expr(self, p): 
        return ('sub', p.expr0, p.expr1) 
  
    @_('expr "*" expr') 
    def expr(self, p): 
        return ('mul', p.expr0, p.expr1) 
  
    @_('expr "/" expr') 
    def expr(self, p): 
        return ('div', p.expr0, p.expr1) 
  
    @_('"-" expr %prec UMINUS') 
    def expr(self, p): 
        return p.expr 
  
    @_('NAME') 
    def expr(self, p): 
        return ('var', p.NAME) 
  
    @_('NUMBER') 
    def expr(self, p): 
        return ('num', p.NUMBER)

O analisador também deve analisar em operações aritméticas, isso pode ser feito por expressões. Digamos que você queira algo como o mostrado abaixo. Aqui, todos eles são transformados em fluxo de token linha por linha e analisados ​​linha por linha. Portanto, de acordo com o programa acima, a = 10 é semelhante à linha 22. O mesmo para b = 20. a + b se assemelha à linha 34, que retorna uma árvore de análise ('add', ('var', 'a'), ('var', 'b')).



Idioma GFG> a = 10
Idioma GFG> b = 20
Idioma GFG> a + b
30

Agora convertemos os fluxos de token em uma árvore de análise. O próximo passo é interpretá-lo.

Execução

A interpretação é um procedimento simples. A ideia básica é pegar a árvore e percorrê-la para avaliar as operações aritméticas hierarquicamente. Este processo é chamado recursivamente repetidamente até que toda a árvore seja avaliada e a resposta seja recuperada. Digamos, por exemplo, 5 + 7 + 4. Este fluxo de caracteres é primeiro tokenizado para fluxo de tokens em um lexer. O fluxo de token é então analisado para formar uma árvore de análise. A árvore de análise essencialmente retorna ('adicionar', ('adicionar', ('num', 5), ('num', 7)), ('num', 4)). (veja a imagem abaixo)

O interpretador vai adicionar 5 e 7 primeiro e, em seguida, chamar recursivamente walkTree e adicionar 4 ao resultado da adição de 5 e 7. Assim, obteremos 16. O código a seguir faz o mesmo processo. 

class BasicExecute: 
    
    def __init__(self, tree, env): 
        self.env = env 
        result = self.walkTree(tree) 
        if result is not None and isinstance(result, int): 
            print(result) 
        if isinstance(result, str) and result[0] == '"': 
            print(result) 
  
    def walkTree(self, node): 
if isinstance(node, int): 
            return node 
        if isinstance(node, str): 
            return node 
if node is None: 
            return None
if node[0] == 'program': 
            if node[1] == None: 
                self.walkTree(node[2]) 
            else: 
                self.walkTree(node[1]) 
                self.walkTree(node[2]) 
if node[0] == 'num': 
            return node[1] 
if node[0] == 'str': 
            return node[1] 
if node[0] == 'add': 
            return self.walkTree(node[1]) + self.walkTree(node[2]) 
        elif node[0] == 'sub': 
            return self.walkTree(node[1]) - self.walkTree(node[2]) 
        elif node[0] == 'mul': 
            return self.walkTree(node[1]) * self.walkTree(node[2]) 
        elif node[0] == 'div': 
            return self.walkTree(node[1]) / self.walkTree(node[2]) 
if node[0] == 'var_assign': 
            self.env[node[1]] = self.walkTree(node[2]) 
            return node[1] 
if node[0] == 'var': 
            try: 
                return self.env[node[1]] 
            except LookupError: 
                print("Undefined variable '"+node[1]+"' found!") 
                return 0

Exibindo a saída

Para exibir a saída do interpretador, devemos escrever alguns códigos. O código deve primeiro chamar o lexer, depois o analisador e, em seguida, o interpretador e, finalmente, recuperar a saída. A saída é então exibida no shell.

if __name__ == '__main__': 
    lexer = BasicLexer() 
    parser = BasicParser() 
    print('GFG Language') 
    env = {} 
      
    while True: 
        try: 
            text = input('GFG Language > ') 
        except EOFError: 
            break
        if text: 
            tree = parser.parse(lexer.tokenize(text)) 
            BasicExecute(tree, env)

É necessário saber que não tratamos nenhum erro. Portanto, o SLY mostrará suas mensagens de erro sempre que você fizer algo que não esteja especificado nas regras que você escreveu.

Execute o programa que você escreveu usando,

python you_program_name.py

Notas de rodapé

O intérprete que construímos é muito básico. Isso, é claro, pode ser estendido para fazer muito mais. Loops e condicionais podem ser adicionados. Recursos de projeto modulares ou orientados a objetos podem ser implementados. Integração de módulo, definições de método, parâmetros para métodos são alguns dos recursos que podem ser estendidos aos mesmos.