Mais detalhes sobre como eu criei um compilador em Python


Introdução

Para quem não sabe, sim, eu criei um compilador em Python. E se você não havia visto o post anterior, recomendo fortemente que leia ele aqui.

Neste post, queria dar mais detalhes sobre como eu implementei uma linguagem de programação compilada 100% em Python como uma resposta mais clara de algumas pessoas que comentaram no primeiro post. Esse post é dedicado única e exclusivamente a leitores do post anterior.

Isso vai ser uma espécie de documentação de código mais simplificado para as pessoas que viram o post anterior.

Esse post é bastante técnico, portanto, se não quiser ler isso, leia o post original Como eu criei um compilador em Python (é serio).

1. Como eu usei o LLVM?

Nesse projeto eu utilizo o binding do LLVM para python llvmlite que foi criado pela equipe do Numba para substituir uma biblioteca legada chamada llvmpy para ter mais confiança no código LLVM.

O llvmlite tem a principal função de criar compiladores JIT (Just-in-time).

2. Estrutura do código simplificada

A primeira coisa que devo dizer aqui é que a implementação da linguagem não utiliza Orientação a Objetos por conta de opinião pessoal e problemas que eu encontrei utilizando POO ao implementar o Pile. Veja o commit 45cd0d0f625a30c70fdb13921ea3a1b400bb2f15 do repositório original para mais informações.

Lexer

O Lexer (também conhecido como Tokenizer) é responsável por separar cada token (qualquer unidade de texto dentro da linguagem) em uma lista.

O Lexer do Pile funciona da seguinte forma:

Eu criei uma função que se chama find_col que basicamente busca um index baseado em um predicado (função de callback) passado como parâmetro dentro de uma string.

O Lexer, basicamente, funciona encontrando esses índices para determinados separadores (como espaço e ") e fazendo slices em cada linha do arquivo do código para conseguir 5 informações cruciais:

  • O arquivo em que o token está posicionado;
  • A linha em que o token está localizado em relação a seu arquivo;
  • A coluna em que o token está localizado em relação a sua linha;
  • O tipo que o token pertence;
  • O valor do token em si.

Parser e type checker

O Parser é conhecido como a estrutura que avalia os tokens e cria uma estrutura de dados chamada de AST (Abstract Syntax Tree) para ser avaliada pelo compilador.

O Type checker serve para realizar testes sobre o programa e checar por erros envolvendo tipos de dados errados e valores errados.

Parser

Um ponto importante para destacar é que nessa linguagem, o conceito de sintaxe é bastante mal-definido por ser uma linguagem Concatenativa que utiliza uma notação que teoricamente não precisa de parser.

O Parser do Pile cria uma sequência de nós (Nodes), cada nó contendo um token e uma indicação de que tipo de nó é (por exemplo, um símbolo, um número inteiro, um número de ponto flutuante ou uma string).

O Parser faz isso percorrendo os tokens do programa e, para cada token, toma decisões baseadas no tipo e valor do token para entender a estrutura do programa.

O Parser também lida com os blocos de controle de fluxo (como if e while) para checar se está tudo ok e não há nenhum tipo de erro na definição dos blocos.

Type Checker

O Type Checker é responsável por verificar se as operações realizadas nos valores da pilha são compatíveis em termos de tipos. Ele garante que as operações sejam feitas apenas em valores apropriados e gera erros se houver um desvio de tipo.

O Type Checker utiliza uma função chamada check_op para verificar se uma operação é valida em termos de tipos. Ele mantém um controle do tipo dos valores na pilha e verifica se os tipos dos valores correspondem aos tipos esperados pelas operações.

O Type checker do Pile também lida com a pilha do programa em si e verifica se não há valores faltando ou sobrando (stack underflow e stack overflow, respectivamente).

3. Compilador LLVM

Essa parte é a mais simples de explicar, pois é auto explicativa.

O compilador vai iterar sobre os Nodes do programa e vai realizando as operações LLVM necessárias para compilar o programa por completo.

Conclusão

Basicamente, expliquei como eu fiz um compilador em Python. O projeto, até agora está completando mais de 1 mês de desenvolvimento e eu estou bastante ansioso para aonde esse projeto pode ir.

Se você quiser me ajudar fazendo contribuições ou só testando o projeto eu fico muito grato!

Acesse o repositório oficial em marc-dantas/pile.