Como é que eu iria criar uma linguagem de programação?

O título deste artigo reflete uma pergunta que ouço repetidamente em fóruns ou em e-mails que recebo.

Pensei que todos os desenvolvedores curiosos perguntaram pelo menos uma vez. É normal ficar fascinado com a forma como as linguagens de programação funcionam. Infelizmente, a maioria das respostas que lemos são muito acadêmicas ou teóricas. Algumas outras contêm demasiados detalhes de implementação. Depois de lê-las ainda nos perguntamos como as coisas funcionam na prática.

Então nós vamos responder. Sim, vamos ver qual é o processo para criar a sua própria linguagem completa com um compilador para isso e o que não.

A visão geral

A maioria das pessoas que querem aprender como “criar uma linguagem de programação” estão efetivamente procurando por informações sobre como construir um compilador. Eles querem entender a mecânica que permite a execução de uma nova linguagem de programação.
Um compilador é uma peça fundamental do puzzle mas fazer uma nova linguagem de programação requer mais do que isso:

1) Uma linguagem tem que ser desenhada: o criador da linguagem tem que tomar algumas decisões fundamentais sobre os paradigmas a serem usados e a sintaxe da linguagem
2) Um compilador tem que ser criado
3) Uma biblioteca padrão tem que ser implementada
4) Ferramentas de suporte como editores e sistemas de construção têm que ser fornecidas

Vejamos mais em detalhes o que cada um desses pontos implica.

Desenhando uma linguagem de programação

Se você quiser apenas escrever seu próprio compilador para aprender como estas coisas funcionam, você pode pular esta fase. Você pode simplesmente pegar um subconjunto de uma linguagem existente ou criar uma simples variação da mesma e começar. Entretanto, se você tem planos para criar sua própria linguagem de programação, você terá que pensar nisso.

Eu penso em desenhar uma linguagem de programação como duas fases divididas:

  1. A fase de grande-imagem
  2. A fase de refinamento

Na primeira fase nós respondemos as perguntas fundamentais sobre nossa linguagem.

  • Que paradigma de execução nós queremos usar? Será imperativo ou funcional? Ou talvez baseado em máquinas de estado ou regras de negócio?
  • Queremos digitação estática ou dinâmica?
  • Em que tipo de programas esta linguagem será melhor? Será usada para pequenos scripts ou grandes sistemas?
  • O que mais nos interessa: desempenho? Legibilidade?
  • Queremos que seja semelhante a uma linguagem de programação existente? Será dirigida a desenvolvedores em C ou fácil de aprender para quem vem de Python?
  • Queremos que funcione em uma plataforma específica (JVM, CLR)?
  • Que tipo de capacidades de metaprogramação queremos suportar, se houver alguma? Macros? Templates? Reflexão?

Na segunda fase vamos continuar a evoluir a linguagem à medida que a utilizamos. Encontraremos problemas, coisas que são muito difíceis ou impossíveis de expressar em nossa linguagem e acabaremos evoluindo-a. A segunda fase pode não ser tão glamorosa como a primeira, mas é a fase em que continuamos a afinar a nossa linguagem para torná-la utilizável na prática, portanto não devemos subestimá-la.

Construir um compilador

Construir um compilador é o passo mais emocionante na criação de uma linguagem de programação. Uma vez que temos um compilador, podemos realmente dar vida à nossa linguagem. Um compilador permite-nos começar a brincar com a linguagem, usá-la e identificar o que nos faz falta no design inicial. Ele permite ver os primeiros resultados. É difícil vencer a alegria de executar o primeiro programa escrito em nossa nova linguagem de programação, não importa quão simples seja esse programa.

Mas como construímos um compilador?

Como tudo que é complexo, fazemos isso em passos:

  1. Construímos um parser: o parser é a parte do nosso compilador que pega o texto dos nossos programas e entende quais comandos eles expressam. Ele reconhece as expressões, as afirmações, as classes e cria estruturas de dados internas para representá-las. O resto do analisador irá trabalhar com essas estruturas de dados, não com o texto original
  2. (opcional) Traduzimos a árvore de parse em uma Árvore de Sintaxe Abstrata. Tipicamente as estruturas de dados produzidas pelo analisador são um nível um pouco baixo, pois contêm muitos detalhes que não são cruciais para o nosso compilador. Por causa disso, queremos frequentemente reorganizar as estruturas de dados em algo um nível ligeiramente mais alto
  3. Resolvemos símbolos. No código nós escrevemos coisas como a + 1. O nosso compilador precisa de perceber a que é que a se refere. É um campo? É uma variável? É um parâmetro de método? Nós examinamos o código para responder que
  4. Validamos a árvore. Precisamos verificar que o programador não cometeu erros. Ele está tentando somar um booleano e um int? Ou a aceder a um campo inexistente? Precisamos produzir mensagens de erro apropriadas
  5. Geramos o código da máquina. Neste ponto traduzimos o código em algo que a máquina pode executar. Pode ser código de máquina apropriado ou bytecode para alguma máquina virtual
  6. (opcional) Realizamos o link. Em alguns casos precisamos combinar o código de máquina produzido para nossos programas com o código das bibliotecas estáticas que queremos incluir, para gerar um único executável

Precisamos sempre de um compilador? Não. Podemos substituí-lo por outros meios para executar o código:

  • Podemos escrever um interpretador: um interpretador é substancialmente um programa que faz os passos 1-4 de um compilador e depois executa diretamente o que é especificado pela Árvore de Sintaxe Abstrata
  • Podemos escrever um transpiler: um transpiler fará o que é especificado nos passos 1-4 e então emitirá algum código em alguma linguagem para a qual já temos um compilador (por exemplo C++ ou Java)

Estas duas alternativas são perfeitamente válidas e frequentemente faz sentido escolher uma destas duas porque o esforço requerido é tipicamente menor.

Escrevemos um artigo explicando como se escreve um transpiler. Dê uma olhada nele se quiser ver um exemplo prático, com code.

Neste artigo explicamos com mais detalhes a diferença entre um compilador e um intérprete.

Uma biblioteca padrão para a sua linguagem de programação

Ainda linguagem de programação precisa fazer algumas coisas:

  • Imprimir na tela
  • Acesso ao sistema de arquivos
  • Utilizar conexões de rede
  • Criar GUIs

Essas são as funcionalidades básicas para interagir com o resto do sistema. Sem elas, uma linguagem é basicamente inútil. Como é que fornecemos estas funcionalidades? Através da criação de uma biblioteca padrão. Esta será um conjunto de funções ou classes que podem ser chamadas nos programas escritos em nossa linguagem de programação, mas que serão escritas em alguma outra linguagem. Por exemplo, muitas linguagens têm bibliotecas padrão escritas pelo menos parcialmente em C.

Uma biblioteca padrão pode então conter muito mais. Por exemplo, classes para representar as coleções principais como listas e mapas, ou para processar formatos comuns como JSON ou XML. Muitas vezes ela conterá funcionalidades avançadas para processar strings e expressões regulares.

Em outras palavras, escrever uma biblioteca padrão é muito trabalho. Não é glamorosa, não é conceitualmente tão interessante quanto escrever um compilador, mas ainda é um componente fundamental para viabilizar uma linguagem de programação.

Existem maneiras de evitar este requisito. Uma delas é fazer a linguagem rodar em alguma plataforma e tornar possível a reutilização da biblioteca padrão de outra linguagem. Por exemplo, todas as linguagens rodando na JVM podem simplesmente reutilizar a biblioteca padrão Java.

Ferramentas de suporte para uma nova linguagem de programação

Para tornar uma linguagem utilizável na prática, frequentemente precisamos escrever algumas ferramentas de suporte.

O mais óbvio é um editor. Um editor especializado com destaque de sintaxe, verificação de erros em linha e auto-completamento é hoje em dia uma necessidade para tornar qualquer desenvolvedor produtivo.

Mas hoje em dia os desenvolvedores são mimados e eles esperam todo tipo de outras ferramentas de suporte. Por exemplo, um depurador pode ser realmente útil para lidar com um bug desagradável. Ou um sistema de construção similar ao maven ou gradle poderia ser algo que os usuários irão perguntar mais tarde.

No início um editor poderia ser suficiente, mas à medida que sua base de usuários crescer, a complexidade dos projetos também crescerá e mais ferramentas de suporte serão necessárias. Esperamos que nesse momento haja uma comunidade disposta a ajudar a construí-las.

Sumário

Criar uma linguagem de programação é um processo que parece misterioso para muitos desenvolvedores. Neste artigo nós tentamos mostrar que é apenas um processo. É fascinante e não é fácil, mas pode ser feito.

Você pode querer construir uma linguagem de programação por uma variedade de razões. Uma boa razão é para diversão, outra é para aprender como os compiladores funcionam. A sua linguagem pode acabar sendo muito útil ou não, dependendo de muitos fatores. No entanto, se você se divertir e/ou aprender enquanto a constrói então vale a pena investir algum tempo nisso.

E é claro que você poderá se gabar com seus colegas desenvolvedores.

Se você quiser aprender mais sobre como criar uma linguagem, dê uma olhada nos outros recursos que criamos: aprenda a construir linguagens.

Você também pode estar interessado em alguns de nossos artigos:

Deixe uma resposta

O seu endereço de email não será publicado.