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:
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?