Spinel: compilador nativo AOT para Ruby
(github.com/matz)- Converte código Ruby em binários nativos standalone, visando execução com média geométrica cerca de 11,6x mais rápida que o
minirubydo CRuby mais recente por meio de inferência de tipos em escala de programa inteiro e geração de código C - O pipeline de compilação transforma Ruby em texto AST com um parser baseado em Prism, depois um backend self-hosting faz inferência de tipos e geração de código C, e um compilador C padrão produz o binário standalone
- O backend do compilador tem uma arquitetura self-hosting escrita em Ruby e, após o processo de bootstrap,
gen2.c == gen3.cse mantém, fechando o loop de recompilar a si próprio - Inclui otimizações em tempo de compilação como achatamento de concatenação de strings, value-type promotion, loop-invariant length hoisting, static symbol interning e promoção automática para bigint, além de um engine de regexp embutido, bigint e runtime em header único para reduzir dependências externas de runtime
- Não oferece suporte a
eval, metaprogramação, Thread nem tratamento geral de encoding, mas demonstra a praticidade da compilação AOT para Ruby com um formato de distribuição que roda sem Ruby e grande diferença de desempenho em cargas computacionalmente intensivas
Como funciona
- O pipeline de compilação funciona parseando arquivos Ruby, serializando-os em arquivos de texto AST, depois aplicando inferência de tipos e geração de código C, e por fim criando um binário nativo com um compilador C padrão
spinel_parseusa Prism e libprism para fazer o parse de Ruby e, quando o binário C não está disponível, usa um caminho alternativo com CRuby e a gem Prismspinel_codegenroda como um binário nativo self-hosted, recebe o AST e realiza inferência de tipos + geração de código C- A etapa final compila o código-fonte C junto com o header do runtime com
cc -O2 -Ilib -lm, e o binário resultante é gerado em formato standalone
Self-Hosting
- A cadeia de bootstrap se fecha produzindo AST com
CRuby + spinel_parse.rb, depoisgen1.cebin1comCRuby + spinel_codegen.rb, e então usando o binário gerado para produzirgen2.cegen3.c gen2.c == gen3.cconfirma que o bootstrap loop foi fechado- O backend
spinel_codegen.rbé escrito em um subconjunto de Ruby que o próprio Spinel consegue compilar- classes,
def,attr_accessor if/case/whileeach/map/select,yieldbegin/rescue- operações com String, Array, Hash e File I/O
- classes,
- O backend não inclui metaprogramming,
evalnemrequire
Desempenho e benchmarks
- Os testes estão em 74 aprovados e os benchmarks em 55 aprovados
- Em 28 benchmarks, a média geométrica é de cerca de 11,6x mais rápido que o
minirubydo CRuby mais recente - A base de comparação é um build recente de
minirubydo CRuby sem gems empacotadas; mesmo comparando com um baseline mais rápido que oruby3.2.3 do sistema, a vantagem continua grande em cargas computacionalmente intensivas -
Desempenho computacional
life: 20ms contra 1.733ms, 86,7x mais rápidoackermann: 5ms contra 374ms, 74,8x mais rápidomandelbrot: 25ms contra 1.453ms, 58,1x mais rápido- versão recursiva de
fib: 17ms contra 581ms, 34,2x mais rápido nqueens: 10ms contra 304ms, 30,4x mais rápidotarai: 16ms contra 461ms, 28,8x mais rápidotak: 22ms contra 532ms, 24,2x mais rápidomatmul: 13ms contra 313ms, 24,1x mais rápidosudoku: 6ms contra 102ms, 17,0x mais rápidopartial_sums: 93ms contra 1.498ms, 16,1x mais rápidofannkuch: 2ms contra 19ms, 9,5x mais rápidosieve: 39ms contra 332ms, 8,5x mais rápidofasta: 3ms contra 21ms, 7,0x mais rápido
-
Estruturas de dados e GC
rbtree: 24ms contra 543ms, 22,6x mais rápidosplay tree: 14ms contra 195ms, 13,9x mais rápidohuffman: 6ms contra 59ms, 9,8x mais rápidoso_lists: 76ms contra 410ms, 5,4x mais rápidobinary_trees: 11ms contra 40ms, 3,6x mais rápidolinked_list: 136ms contra 388ms, 2,9x mais rápidogcbench: 1.845ms contra 3.641ms, 2,0x mais rápido
-
Programas reais
json_parse: 39ms contra 394ms, 10,1x mais rápido- cálculo de
bigint_fibcom 1000 dígitos: 2ms contra 16ms, 8,0x mais rápido ao_render: 417ms contra 3.334ms, 8,0x mais rápidopidigits: 2ms contra 13ms, 6,5x mais rápidostr_concat: 2ms contra 13ms, 6,5x mais rápidotemplate engine: 152ms contra 936ms, 6,2x mais rápidocsv_process: 234ms contra 860ms, 3,7x mais rápidoio_wordcount: 33ms contra 97ms, 2,9x mais rápido
Recursos Ruby suportados
- Como recursos Core, oferece suporte a classes, herança,
super, mixins cominclude,attr_accessor,Struct.new,alias, constantes de módulo e open classes para tipos embutidos - Em Control Flow, oferece suporte a
if/elsif/else,unless,case/when, pattern matching comcase/in,while,until,loop,for..in,break,next,return,catch/throwe&. - Em Blocks, oferece suporte a
yield,block_given?,&block,proc {},Proc.new,-> x { },method(:name), além de métodos com bloco comoeach,map,select,reduce,sort_by,times,uptoedownto - Em Exceptions, oferece suporte a
begin/rescue/ensure/retry,raisee classes de exceção definidas pelo usuário - Types inclui Integer, Float, String, Array, Hash, Range, Time, StringIO, File, Regexp, Bigint e Fiber
- Valores polimórficos são tratados como tagged unions
- Há tipos de objeto anuláveis
T?para estruturas de dados autorreferentes
- Global Variables compila
$namecomo variáveis C estáticas e detecta incompatibilidades de tipo em tempo de compilação - I/O oferece suporte a
puts,print,printf,p,gets,ARGV,ENV[],File.read/write/open,system()e crases
Strings, regexp, símbolos, Bigint e Fiber
- Strings lidam tanto com strings imutáveis quanto mutáveis, e
<<promove automaticamente para a string mutávelsp_Stringpara fazer append in-place em O(n) +, interpolação,tr,ljust/rjust/centere métodos padrão funcionam nas duas representações de string- Comparações como
s[i] == "c"são otimizadas para acessar diretamente o array de chars, sendo processadas sem alocação - Concatenações como
a + b + c + dsão achatadas em uma única chamadasp_str_concat4ousp_str_concat_arr, reduzindo N-1 alocações str.split(sep)dentro de loops reutiliza repetidamente o mesmosp_StrArray, removendo 4 milhões de alocações emcsv_process- Regexp usa um engine NFA de regexp embutido, sem dependências externas
- Oferece suporte a
=~,$1-$9,match?,gsub,sub,scanesplit
- Oferece suporte a
- Bigint usa inteiros de precisão arbitrária baseados em mruby-bigint
- Há promoção automática em padrões de multiplicação em loop como
q = q * k - É linkado como biblioteca estática e só é incluído quando realmente usado
- Há promoção automática em padrões de multiplicação em loop como
- Fiber oferece concorrência cooperativa baseada em
ucontext_t- Suporta
Fiber.new,Fiber#resume,Fiber.yielde passagem de valores - Variáveis livres são capturadas como células promovidas para heap
- Suporta
- Symbols são implementados como um tipo
sp_symseparado de strings:a != "a"é preservado- Literais de símbolo são internados em tempo de compilação e viram constantes
SPS_name String#to_symusa um pool dinâmico apenas quando necessário- Hashes com chaves símbolo usam
sp_SymIntHash, armazenando diretamente chaves inteiras em vez de strings, eliminando strcmp e alocações dinâmicas de string
Gerenciamento de memória e value types
- O gerenciamento de memória usa GC mark-and-sweep, incluindo free lists segregadas por tamanho, marcação não recursiva e sticky mark bits
- Classes pequenas e simples são promovidas automaticamente a value types e colocadas na stack
- Condições: até 8 campos escalares
- sem herança
- sem mutação via parâmetros
- A alocação de uma classe com 5 campos, repetida 1 milhão de vezes, cai de 85ms para 2ms
- Programas que usam apenas value types não exportam o runtime de GC
Otimizações
- Com base na inferência de tipos em escala de programa inteiro, o compilador realiza várias otimizações em tempo de compilação
- Value-type promotion transforma pequenas classes imutáveis em structs C alocadas na stack, eliminando overhead de GC
- Constant propagation faz constantes literais simples como
N = 100serem inlined diretamente no ponto de uso, sem busca porcst_N - Loop-invariant length hoisting faz
while i < arr.lengthcalcular o tamanho apenas uma vez antes do loop- Se o objeto receptor for alterado no corpo, como com
arr.push, esse hoist é desativado
- Se o objeto receptor for alterado no corpo, como com
- Method inlining adiciona
static inlinea métodos curtos, não recursivos e com até 3 instruções, induzindo o inlining pelo gcc - String concat chain flattening reduz cadeias de concatenação a uma única chamada, eliminando strings intermediárias
- Bigint auto-promotion promove automaticamente padrões de soma autorreferente ou multiplicação repetida para bigint
- Bigint
to_susampz_get_strdo mruby-bigint para processamento divide-and-conquer em O(n log²n) - Static symbol interning converte
"literal".to_symem constantes de tempo de compilaçãoSPS_<name>; o pool de intern dinâmico só entra quando necessário - Em
sub_range, strings com comprimento içado usamsp_str_sub_range_lenpara pular chamadas internas astrlen line.split(",")dentro de loops reutiliza osp_StrArrayexistente- Dead-code elimination usa
-ffunction-sections -fdata-sectionse--gc-sectionspara remover funções de runtime não usadas do binário final - Iterative inference early exit interrompe imediatamente o loop de ponto fixo quando os 3 arrays de assinatura de param, return e ivar deixam de mudar
- A maioria dos programas converge em 1 a 2 iterações em vez de 4
- O tempo de bootstrap cai cerca de 14%
parse_id_listbyte walk trocas.split(",")por uma varredura manual coms.bytes[i]no parser de listas de campos do AST, chamado cerca de 120 mil vezes na self-compilation, reduzindo as alocações por chamada de N+1 para 2- O código C gerado mantém build sem warnings no nível padrão de alertas, e o harness usa
-Werrorpara expor regressões imediatamente
Arquitetura
- A estrutura do repositório se divide nos seguintes componentes
spinel: script wrapper de um comando só baseado em shell POSIXspinel_parse.c: frontend C de 1.061 linhas, de libprism para AST em textospinel_codegen.rb: backend do compilador de 21.109 linhas, de AST para código Clib/sp_runtime.h: header da biblioteca de runtime com 581 linhaslib/sp_bigint.c: inteiros de precisão arbitrária com 5.394 linhaslib/regexp/: engine de regexp embutido com 1.759 linhastest/: 74 testes funcionaisbenchmark/: 55 benchmarksMakefile: automação de build
- O runtime
lib/sp_runtime.hreúne em um único header o GC, a implementação de arrays/hash/strings e outros suportes de runtime - O código C gerado inclui esse header, e o linker puxa apenas as partes necessárias de
libspinel_rt.a- bigint
- regexp engine
- O parser tem duas implementações
spinel_parse.clinka diretamente com libprism e roda sem CRubyspinel_parse.rbé o fallback em CRuby que usa a gem Prism
- Os dois parsers produzem a mesma saída AST, e o wrapper
spinelprioriza o binário C sempre que possível require_relativeé resolvido no momento do parsing, e os arquivos referenciados são inlined
Limitações
- No eval:
eval,instance_evaleclass_evalnão são suportados - No metaprogramming:
send,method_missingedefine_methoddinâmico não são suportados - No threads:
ThreadeMutexnão são suportados; apenas Fiber é suportado - No encoding: assume UTF-8 e ASCII
- No general lambda calculus: não lida com
-> x { }profundamente aninhado nem chamadas[]
Dependências e modelo de execução
- As dependências de build são a biblioteca C libprism e CRuby para o bootstrap inicial
- Não há dependências de runtime; os binários gerados exigem apenas libc + libm
- Regexp usa engine embutido, então não requer biblioteca externa
- Bigint é embutido, mas só é linkado quando realmente usado
- Prism é o parser Ruby usado por
spinel_parsemake depsbaixa o tarball da gem prism do rubygems.org e extrai o código C emvendor/prism- Se a gem prism já estiver instalada, ela é detectada automaticamente
- Também é possível indicar um caminho personalizado com
PRISM_DIR=/path/to/prism
- CRuby é necessário apenas no bootstrap inicial; depois de
make, todo o pipeline roda sem Ruby
Histórico do projeto
- Spinel foi implementado inicialmente em C, com 18K lines, ainda preservadas na branch
c-version - Depois passou pela branch
ruby-v1, reescrita em Ruby - O
masteratual é a versão reescrita em um subconjunto de Ruby capaz de fazer self-hosting
Licença
- Usa a licença MIT
- Segue o arquivo LICENSE
1 comentários
Opiniões no Hacker News
Se foi o Matz que fez, então ele certamente conhece bem os limites da semântica do Ruby, o que passa confiança
Minha dissertação de mestrado também foi sobre um compilador AOT de JS; ele até funcionava, mas as restrições sobre os dados de entrada eram grandes demais, então acabei abandonando
Na época, os desenvolvedores de JS não estavam acostumados a seguir esse tipo de restrição por conta própria, e entradas inerentemente desconhecidas, como
JSON.parse, eram um grande obstáculoHoje, com TypeScript, isso pode ser bem mais viável do que naquela época
Mesmo olhando apenas para o cálculo lambda em geral, os limites da inferência de tipos são claros, e artigos do Matt Might ou trabalhos como Shed-skin para Python mostram restrições parecidas
Fico curioso para saber o quão comuns são
eval,send,method_missingedefine_methodem código Ruby real, e também como normalmente lidam com parsing sem tipos, por exemplo entradas JSONFazer o parsing de Ruby é tão difícil que chega a ser mais complicado do que a própria tradução, então eles usam o Prism, e o resultado gerado é C
A semântica básica do Ruby em si não é tão difícil de implementar assim
Em contrapartida, eu estou preso a um antigo compilador AOT self-hosted feito em Ruby puro, e escolhi de propósito um caminho muito mais difícil ao insistir em usar um parser próprio
Aprendi cedo que os primeiros 80% podem ser montados mais ou menos e ainda assim uma boa parte do código Ruby já roda; o verdadeiro "segundo 80%" difícil está concentrado nas coisas que o Matz deixou de fora neste projeto e no mruby, como codificação e todo tipo de recurso periférico
Sinceramente, o Ruby tem várias funcionalidades que eu nunca vi em código real, então não acharia estranho se algumas fossem descontinuadas
send,method_missingedefine_methodsão muito comunsAs restrições são parecidas com as do mruby, e ainda existe espaço de uso mesmo dentro delas
Suporte a
send,method_missingedefine_methodé relativamente fácilJá suporte a eval() é extremamente doloroso
Ainda assim, uma grande parte do uso de
eval()em Ruby pode ser reduzida estaticamente à versão com bloco deinstance_eval, e nesses casos a compilação AOT fica bem mais simplesPor exemplo, se a string passada para
eval()puder ser conhecida estaticamente ou decomposta, há bastante margem para resolverNa prática, muitos usos de
eval()são desnecessários ou apenas uma forma simples de contornar limitações de introspecção, então dá para tratar com análise estáticaNo meu compilador, se isso virar gargalo, pretendo atacar essa parte primeiro
Ingestão de JSON sem tipos provavelmente também tende a usar esses mecanismos
Se você remover isso, sobra uma linguagem pequena e fácil de ler, não tão fortemente tipada quanto Crystal, mas também sem depender de metaprogramação tanto quanto o Ruby oficial
Então o potencial parece bem grande, mas no fim só o tempo vai dizer
evalcom frequênciaTalvez desse para não usar, mas para mim ele é mais ergonômico
eval,exec,define_methode o padrão de criar novas classes comClass.neweStruct.newA maior parte desses usos se concentra no boot da aplicação ou enquanto arquivos estão sendo carregados com require, o que em certo sentido já é parecido com uma etapa de compilação
Isso foi apresentado agora há pouco pelo Matz na RubyKaigi 2026
É experimental, mas foi feito em cerca de um mês com ajuda do Claude, e a demo ao vivo deu certo
O nome veio do novo gato do Matz, e o nome do gato veio do gato de Card Captor Sakura, que por sua vez faz par com uma personagem chamada Ruby
No caso de alguém como o Matz, talvez seja mais como levar de 100x para 500x
https://en.wikipedia.org/wiki/Spinel
Parece que o vídeo ainda não está ao vivo, e eles vão subindo um por um neste canal
https://www.youtube.com/@rubykaigi4884/videos
O nome do projeto também dá a sensação de ter sido escolhido de forma emocional
Sem dúvida é muito impressionante, mas parece impossível de manter sem um agente de IA
spinel_codegen.rbtem 21 mil linhas, e alguns métodos chegam a 15 níveis de aninhamentoCódigo de compilador já tende a ser difícil de deixar bonito, mas isso parece muito complicado de manter por humanos até mesmo para esse padrão
Compiladores têm fronteiras de subsistemas bem definidas e handoffs claros entre etapas, então, na verdade, estão entre as coisas mais fáceis de modularizar
O problema costuma ser fazer funcionar primeiro e depois nunca sobrar tempo para refatorar, e aí a sujeira só vai crescendo
spinel_codegen.rbestá quase no nível de um horror lovecraftianoQuando uso Claude, meu código sempre acaba virando esse tipo de espaguete, então eu estava me perguntando se estava fazendo algo errado
Mas ver qualidade de código bem ruim em vários pontos até num projeto realmente interessante feito por alguém que considero um programador de altíssimo nível me mostrou que não sou só eu
Por exemplo,
infer_comparison_type()não é o pior caso e nem é ilegível, mas existe uma implementação muito mais simples e clara, e o Claude não chega nelaSe reunisse os operadores de comparação em um
Sete tratasse cominclude?, ficaria mais curto, mais rápido, mais legível e mais fácil de manterMas o Claude sempre cai numa cadeia de if-return, e até parece desconfortável até com if-else
Minha base de código gerada pelo Claude também está cheia desse padrão, então agora sei que não sou o único
Em compensação, os outros arquivos estão bem melhores, especialmente o diretório
lib, que parece corresponder ao diretórioextdo repositório principal do Ruby e tem qualidade razoávelA API também é claramente influenciada pelo MRI Ruby, e mesmo que a implementação seja bem diferente, parece que o Matz guiou para que ela lembrasse parte da API original, então a saída ficou mais organizada
[1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
Se os testes e benchmarks passarem, já fico satisfeito por enquanto
Ainda assim, fico em dúvida se arquivos gigantes também são fáceis para IA lidar
Eu tento manter arquivos abaixo de 300 linhas e acho que código fácil para humanos entenderem também deve ser mais fácil para agentes de código
As restrições parecem ser estas
Sem eval:
eval,instance_eval,class_evalSem metaprogramação:
send,method_missing,define_method(dinâmico)Sem threads:
Thread,Mutex(Fiber é suportado)Sem encoding: suposição de UTF-8/ASCII
Sem cálculo lambda geral:
-> x { }profundamente aninhado com chamada de[]A suposição de UTF-8/ASCII, pessoalmente, não me parece uma limitação tão grande, mas o restante parece ser uma restrição real para bastante programa
E parece que dar suporte a isso de novo exigiria bastante trabalho
Uso Ruby há muito tempo e já usei todos os recursos listados, e olhando por esse lado, o que eu acabei querendo com o tempo foi justamente uma versão assim de Ruby simples
É mais simples e mais fácil de entender, mas ainda preserva a estética própria do Ruby
Agora, com LLMs, a produtividade de geração de código é tão alta que já há menos necessidade de reduzir boilerplate com metaprogramação em nome da produtividade do desenvolvedor
Isso porque o peso do código escrito manualmente pelo próprio desenvolvedor está diminuindo
A sintaxe é parecida e há um sistema de tipos estático, o que leva a código compilado mais eficiente
eval, mas a ausência de threads e mutexes é uma penaA falta de
define_methodeu entendo pelo tipo de uso que ele temMas
sendemethod_missingsão comuns em bibliotecas existentes, e também não me parecem tão difíceis de implementar, por exemplo montando uma tabela de lookup em memória em tempo de compilaçãoEntão não sei se isso foi removido de propósito ou se simplesmente ainda não chegaram lá
Espero que seja a segunda opção, mas ao menos por enquanto a compatibilidade deve impedir uso em produção
Sempre foi reduzir a quantidade de código que precisa ser lida
Isso é muito legal, e eu espero há muito tempo por um compilador AOT para Ruby
Só é uma pena não haver fallback para
evalou metaprogramação, mas imagino que a ideia tenha sido focar num subconjunto pequeno e de alto desempenhoEu gostaria que gems feitas com esse compilador AOT interagissem bem com o MRI
Empacotar ou fazer bundle do Ruby padrão com gems ainda depende de tebako, kompo, ocran, e antes também havia projetos como ruby-packer, traveling ruby e jruby warbler
É bom ter mais uma opção, mas continuo torcendo por uma solução definitiva com UX melhor para desenvolvedores
Fazia tempo demais que ele não recebia atualizações
Fico curioso sobre o motivo de não haver threads
O scheduler do Ruby e a implementação subjacente em pthread parecem coisas que poderiam funcionar bem também no lado C, então me pergunto se a ideia era buscar dependência zero
Se não houver plano de adicionar isso como extensão opcional depois, ou se não for algo apenas ainda não implementado, essa escolha parece meio estranha
Acho mais provável que simplesmente ainda não tenham chegado lá
Multithreading já é algo muito difícil de fazer direito por natureza
É surpreendente que isso tenha sido feito em pouco mais de um mês
Pode-se dizer o que for sobre IA, mas nas mãos de um desenvolvedor talentoso ela produz um ganho enorme de velocidade
Já o Matz parece funcionar só com
gem env|infoefindComo isso foi feito pelo próprio Matz, fico curioso sobre o quão realista é a chance de isso virar parte do core do Ruby no futuro
E, se isso acontecer, também me pergunto o quanto isso seria uma ameaça para o Crystal
Essas características são praticamente indispensáveis para compilar e manter programas grandes
Já isso aqui é voltado a um subconjunto limitado de Ruby, então a maioria dos gems populares de Ruby provavelmente não vai rodar como está
Como subconjunto de linguagem voltado a compilar para C, isso parece mais próximo de PreScheme
Neste estágio, eu não diria que os dois estão competindo diretamente no mesmo espaço
Ruby completo quase certamente vai precisar de JIT
[1]: https://prescheme.org/
Seria a revanche de ferramentas como Rational Unified Process e Enterprise Architect
A diferença é que, no lugar de diagramas UML, viriam arquivos markdown
Isso parece útil para o lado de ferramentas de infraestrutura
Por exemplo, dá para imaginar um bundler escrito em Ruby, mas compilado estaticamente, que também cumpra o papel de ferramenta de instalação do Ruby como o RVM
Os buildpacks Ruby atuais são escritos em Ruby, mas exigem bootstrap com bash, o que é irritante e cria edge cases
O CNB foi escrito em Rust para evitar esse problema, e a ideia de distribuir um binário único sem dependências é realmente muito poderosa