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.