Em setembro de 2026, a Rails World aconteceu em Austin, a primeira edição nos Estados Unidos. Como era de se esperar, quase toda palestra passava por IA. Mas o que ficou comigo foi ver como o Rails está chegando a lugares aonde ele não costumava ir.
Trabalho com Ruby on Rails há anos, e durante quase todo esse tempo a conversa foi a mesma. Rails é ótimo para começar mas, quando o assunto é desempenho ou um binário pequeno para distribuir, a resposta costuma ser: outra linguagem.
Só que eu já vinha acompanhando uma coisa que mudaria essa conversa. Em abril de 2026, na RubyKaigi, Matz apresentou o Spinel, um compilador de Ruby. O Spinel lê o programa inteiro, descobre o tipo de cada variável, parâmetro e retorno, e gera código C, que o compilador C do sistema transforma num binário. O resultado é um executável nativo, que roda numa máquina sem depender do Ruby.
Pra entender por que isso é possível, e por que não é trivial, vale entender o que muda quando o Ruby é compilado: as perguntas que ele normalmente responde enquanto o programa roda precisam ser respondidas antes.
Trocando a roda com o carro em movimento
Imagine o seguinte método:
def total(price, quantity)
price * quantity
end
puts total(10, 3)
Quando o Ruby chega em price * quantity, ele ainda não sabe o que é price. Pode ser um Integer, um Float, um BigDecimal ou um objeto seu que define *. Então, a cada chamada, ele só confere o que recebeu antes de decidir como fazer a conta e na próxima chamada, ele confere de novo. Isso é o que deixa o Ruby tão flexível, e também o que o deixa mais lento do que poderia ser. O YJIT ajuda, mas a análise continua acontecendo durante a execução.
O Spinel responde antes
./spinel total.rb
./total
Na compilação, o Spinel olha todas as chamadas a total no programa. Se todas passam inteiros, ele já sabe que price e quantity são sempre inteiros, e price * quantity vira uma multiplicação de inteiros em C. Ninguém precisa conferir nada em tempo de execução. O ganho aparece nos benchmarks mostrados no README: o Spinel sai em média cerca de 8 vezes mais rápido que o Ruby 4 com YJIT.
Mas resolver tudo antes tem um preço: tudo o que só se resolve com o programa rodando fica de fora. Por exemplo:
evalde uma string;method_missing;Class.newem tempo de execução;define_methodcom um nome montado na hora;class Stack < Array.
Os hooks included e inherited até podem ser definidos, mas nunca disparam. O include é resolvido na compilação, então não existe um momento, com o programa rodando, em que ele “acontece”.
Isso significa que o Spinel recusa toda metaprogramação? Não. Coisas como send(:save) funcionam, define_method(:name) com um literal também, e instance_eval com bloco compila. A pergunta que separa o que entra do que fica de fora é uma só:
Dá para saber, só lendo o código, o que essa linha vai fazer?
Quando não dá, o Spinel avisa ou recusa na compilação. Este programa, por exemplo, roda em qualquer Ruby:
class Stack < Array
def peek = last
end
s = Stack.new
s.push(1)
p s.peek
O Spinel não gera o binário e explica o motivo:
spinel: class Stack < Array: subclassing Array is not supported yet (a subclass
would answer differently from CRuby); wrap an Array in an instance variable instead
Ou seja: herdar de Array ainda não é suportado, porque a subclasse se comportaria diferente do CRuby, e a sugestão é guardar um Array numa variável de instância.
Em tese, isso não vai mudar: o Spinel vai continuar sendo um subconjunto de Ruby, porque o CRuby é o oráculo. Cada teste do projeto é um programa .rb com a saída esperada ao lado, gerada rodando o programa no CRuby e se o resultado do binário no Spinel retorna outra coisa, o bug é do Spinel.
E o Rails?
Pra quem já teve a curiosidade de abrir o código do Rails pra ver como ele é feito por dentro, talvez tenha percebido o problema. O Rails é cheio de metaprogramação, e boa parte dela só se resolve em tempo de execução:
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true
end
O Active Record só descobre as colunas consultando o banco e só cria os métodos na hora. O has_many, por exemplo, define métodos quando a classe é carregada. É exatamente o tipo de coisa que o Spinel deixa de fora. Mas o mais louco é que o Campfire, o app de chat da 37signals, já roda compilado pelo Spinel!
Talvez você esteja se perguntando como, depois de tudo o que acabei de explicar sobre o Spinel. A resposta é que o código que você escreve não é o código que o Spinel compila. Antes dele, outra ferramenta, o Roundhouse, do Sam Ruby, faz o trabalho pesado: lê a aplicação Rails e troca as perguntas que o Rails só responde em tempo de execução por código que já traz as respostas. Isso significa que esse mesmo Article sai do outro lado mais ou menos assim:
class Article < ApplicationRecord
attr_accessor :id, :title, :body, :created_at, :updated_at
def comments
@_comments ||= ArticleCommentsProxy.new(self)
end
def validate
validates_presence_of(:title)
end
end
As colunas saem das migrations, o has_many vira um método comum, e o validates vira uma chamada explícita. Não sobrou nenhuma pergunta para responder com o programa rodando. Essa reescrita tem os seus próprios truques e limites. O Roundhouse não é perfeito, e o Spinel compila só uma parte do Ruby. Mas, juntos, eles levam aquela história do início para um rumo diferente.
Entendido isto, a gente deveria escrever o código pensando em como ele vai ser reescrito, ou em como ele vai ser interpretado? O que é mais importante, a flexibilidade ou a velocidade? E se a gente pudesse ter os dois, com um pouco de trabalho extra? Em parte, é o que já temos com o Rails, e é o que o Spinel e o Roundhouse estão tentando levar adiante.