1013 palavrasRead in English

Seu app Rails pode virar um binário

, em Ruby, Rails, Spinel, Compiladores

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:

  • eval de uma string;
  • method_missing;
  • Class.new em tempo de execução;
  • define_method com 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.