Lucas Borges 24 dias atrás

"Criando" um Compilador de Ruby em Ruby

Desde que comecei a mexer com programação, sempre tive aquela curiosidade meio masoquista de entender como as linguagens funcionam por baixo dos panos. Tipo, beleza, a gente escreve um código, roda e magia acontece. Mas e se a gente quisesse criar a nossa própria magia? Foi nessa que me surgiu a ideia: e se eu escrever sobre um compilador de Ruby em Ruby?
Bom gosto, afinal.


Mas pera, o Ruby já não é interpretado?

Pois é, Ruby é uma linguagem interpretada, mas isso não impede a gente de criar um compilador que converta código Ruby para outra coisa. Pode ser bytecode, Assembly ou até mesmo código em outra linguagem. O objetivo aqui não é substituir o MRI (o interpretador Ruby padrão), mas entender melhor como um compilador funciona e, de quebra, aprender um monte sobre parsing, análise léxica e geração de código.

Primeiros Passos: Analisador Léxico (Lexer)

Antes de compilar qualquer coisa, a gente precisa entender o que está sendo escrito. O primeiro passo é transformar o código-fonte em tokens, que são as menores unidades significativas da linguagem. Tipo, se eu tiver esse código:

x = 10 + 20

Nosso lexer precisa quebrar isso em tokens:

IDENTIFIER(x)
ASSIGN(=)
NUMBER(10)
PLUS(+)
NUMBER(20)

Pra fazer isso em Ruby, podemos usar expressões regulares e uma estrutura que percorre o código-fonte caractere por caractere.

Parsing: Transformando Tokens em uma Árvore Sintática

Depois de ter os tokens, a gente precisa organizar eles numa estrutura que faça sentido. Aqui entra o parser, que vai transformar essa lista de tokens em uma AST (Abstract Syntax Tree). A AST representa a estrutura do código de forma hierárquica. No exemplo acima, teríamos algo assim:

      (=)
     /   \
    x     (+)
         /   \
       10    20

Isso facilita muito na hora de gerar código, porque agora temos uma estrutura organizada que representa a intenção do código-fonte.

Geração de Código

Agora que temos a AST, precisamos gerar algo que possa ser executado. Aqui temos algumas opções:

  • - Interpretar diretamente: Percorrer a AST e executar as operações.
  • - Gerar código em outra linguagem: Por exemplo, traduzir para bytecode de uma VM customizada ou até mesmo para C.
  • - Gerar Assembly: Se quiser hardcore mode, pode transformar a AST diretamente em Assembly.

Pra manter as coisas simples, dá pra fazer um interpretador direto da AST. Exemplo básico:

def evaluate(node)
  case node[:type]
  when :number
    node[:value]
  when :binary_op
    left = evaluate(node[:left])
    right = evaluate(node[:right])
    case node[:operator]
    when '+' then left + right
    when '-' then left - right
    else raise "Operação desconhecida"
    end
  end
end

Com isso, dá pra rodar operações matemáticas simples só percorrendo a árvore.

Valeu a Pena?

A real é que tentar criar um compilador é um baita exercício mental. Dá pra entender como linguagens funcionam, o que acontece quando a gente roda um código e, de quebra, ainda se divertir bastante (se você for do tipo que curte quebrar a cabeça com esse tipo de coisa).

Se ficou curioso, recomendo dar uma olhada em como funciona o YARV (o interpretador Ruby) e brincar de criar uma linguagem do zero. Quem sabe um dia a gente faz um Ruby compilado de verdade?

Ouça no mesmo tema