2 pontos por GN⁺ 4 시간 전 | 2 comentários | Compartilhar no WhatsApp
  • Para resolver falhas estruturais da implementação existente, 300 mil linhas em Rust foram reescritas em Zig; após 487 dias, chegou-se à paridade funcional, com a meta de lançar ainda neste ano a primeira versão numerada oficial, a 0.1.0
  • O novo compilador oferece suporte a hot code loading e cross-compilation reproduzível, interpolação de strings dentro de pattern matching e remoção de alocações no heap no roteamento HTTP; também reduziu o tamanho em wasm do Rocci Bird para 31 KB, menos da metade do anterior
  • Os principais motivos para escolher Zig foram tempo de build, alocadores granulares e controle de layout de dados, um ecossistema adequado ao desenvolvimento de compiladores e verificação de código sem segurança de memória; a build incremental do Zig 0.17.0 recompila cerca de 460 mil linhas em 35 ms
  • Na classificação de bugs reais, o compilador em Rust teve 21 casos de corrupção de memória e o compilador em Zig teve 10, mas a maioria se devia a geração incorreta de código; os erros de segurança de memória no próprio compilador Zig foram 2 casos de use-after-free que corrompiam nomes de arquivos
  • Zig se encaixou bem em estruturas de dados sem ponteiros, desserialização sem parsing e reutilização do serializador de LLVM bitcode, mas a experiência de desenvolvimento em Rust foi melhor em liberação automática de memória em testes, polimorfismo, campos privados de structs, detecção de código morto e compatibilidade entre releases

Reescrita que chegou à paridade funcional

  • A equipe do compilador Roc reescreveu cerca de 300 mil linhas de Rust em Zig ao longo de aproximadamente um ano e meio, chegando à paridade funcional com o compilador existente
  • A paridade funcional é um marco importante, mas não é um lançamento oficial; a versão 0.1.0 do novo compilador está prevista para o fim deste ano
  • Tornou-se possível compilar o jogo de WASM-4 de 2024 Rocci Bird com o novo compilador
    • O jogo inteiro tem menos de 1.000 linhas de código Roc
    • O código-fonte atualizado é mais conciso que o original
    • O binário wasm gerado por roc build --opt=size tem 31 KB, menos da metade do resultado do compilador anterior
  • No site, um compilador WebAssembly de 2,5 MB permite escrever e executar no navegador um programa Roc básico
  • A reescrita levou 487 dias, 476 dias a mais que os 11 dias que a Bun levou para migrar cerca de 500 mil linhas de Zig para Rust
    • A Bun fez um porte direto, enquanto a Roc esteve mais próxima de uma reescrita que alterou significativamente várias estruturas e funcionalidades do compilador
    • Portanto, é difícil comparar diretamente o tamanho ou a duração dos dois codebases apenas em termos de Rust e Zig

Experiência de desenvolvimento oferecida pelo novo compilador

  • Hot code loading e cross-compilation

    • Depois de executar um servidor com roc server.roc, ao modificar o código, o novo código é aplicado automaticamente a partir da próxima requisição
    • É um comportamento comum em linguagens interpretadas como Python, mas pouco comum em linguagens compiladas de alto desempenho como Roc
    • Funciona da mesma forma não só em servidores web, mas também em jogos 2D simples
    • Na hora de distribuir, roc build server.roc gera um binário standalone com otimizações do LLVM aplicadas
    • Com roc build --target=x64musl, é possível fazer cross-compilation de um binário estático para Alpine Linux
    • Ao receber os mesmos bytes de código-fonte como entrada, qualquer sistema de build, inclusive Mac, gera os mesmos bytes de saída
  • Interpolação de strings dentro de pattern matching

    • Interpolações de string como "/users/${id}" são suportadas dentro de pattern matching
    • Em vez de fazer parsing de strings de template em tempo de execução, como no roteamento ao estilo Express, o compilador trata isso diretamente
    • É possível fazer matching simultâneo do método HTTP e do caminho, e ramificar por padrões também em caminhos aninhados ou valores padrão
    • O código completo de tratamento de requisições HTTP do exemplo verifica a segurança de tipos em tempo de compilação e não realiza nenhuma alocação no heap
    • Usando execução em tempo de compilação de funções puras, o roteamento de requisições HTTP foi implementado sem alocação, e a sintaxe pode ser testada no compilador WebAssembly do site

Por que reescrever do zero

  • Roc não é uma linguagem de sistemas, ao contrário de Rust, C e Zig, e usa gerenciamento automático de memória baseado em contagem de referências
    • Evita pausas de garbage collectors com rastreamento
    • Aproveita a otimização Perceus e alterações oportunistas no estilo Koka
  • Ao contrário de linguagens não sistêmicas comuns, não aloca heap a cada captura de closure; em vez disso, usa defuncionalização polimórfica por especialização de conjuntos de lambdas
  • A defuncionalização, em linguagens funcionais, permite várias otimizações posteriores, como inlining, mas tornar a implementação existente correta era extremamente difícil
  • O protótipo em OCaml de Ayaz Hafiz confirmou que o problema era uma falha estrutural espalhada por várias etapas do compilador, e que resolvê-lo exigiria reescrever a maior parte do compilador
  • Outros contribuidores também planejavam reescrever diversas partes por motivos diferentes, então a equipe passou a considerar uma reescrita completa em vez de uma abordagem do navio de Teseu, substituindo quase tudo gradualmente
  • Em projetos de compiladores bem-sucedidos, é comum haver reescritas do zero, por exemplo para self-hosting, e há também o caso da reescrita do TypeScript para Go, que não é a linguagem do próprio TypeScript
  • Embora a política seja que o compilador Roc não faça self-hosting, desta vez a equipe concluiu que os benefícios da reescrita superavam os custos conhecidos

Critérios para escolher Zig em vez de Rust novamente

  • A equipe já usava Zig em várias funcionalidades básicas da biblioteca padrão e, como as únicas linguagens de sistemas que conhecia bem o suficiente eram Rust e Zig, avaliou seriamente apenas essas duas
  • Cada projeto tem uma linguagem adequada diferente, e continuar usando Rust em outros trabalhos não contradiz a escolha de Zig para Roc
  • Tempo de build

    • As builds com cargo em Rust demoravam muito, inclusive builds incrementais, e continuaram sendo um grande incômodo à medida que o codebase crescia
    • A expectativa era que a migração para Zig tornasse as builds muito mais rápidas
  • Controle de memória e layout de dados

    • Cada etapa de compilação usa vários alocadores, especialmente alocadores de arena, e o layout struct of arrays (SoA) é amplamente utilizado
    • O ecossistema Rust em geral pressupõe um único alocador global, enquanto o ecossistema Zig adota por padrão a passagem de alocadores granulares e também oferece suporte a SoA na biblioteca padrão
  • Escopo do ecossistema necessário

    • O ecossistema Rust como um todo é maior que o de Zig, mas em ambos havia poucos pacotes relacionados às necessidades específicas da Roc
    • Funcionalidades de nicho necessárias, como código que gera bitcode rapidamente em vez de envolver as bibliotecas C++ do LLVM, eram mais numerosas no lado de Zig
  • Suporte a código sem segurança de memória

    • As 300 mil linhas do compilador Rust existente tinham cerca de 1.200 usos de unsafe
    • Como comparação, o próprio compilador Rust usa cerca de 40 mil unsafe em 3,5 milhões de linhas
    • Em um compilador que gera código de máquina, operações sem segurança de memória são uma parte importante do trabalho
    • Rust segue um modelo de isolar blocos unsafe raros e verificá-los com Miri ou Valgrind, mas em Roc o unsafe não era raro
    • Zig oferecia mais recursos para fazer código sem segurança de memória funcionar corretamente, e era exatamente nessa área que a equipe queria mais apoio

Segurança de memória obtida sem borrow checker

  • Escopo das verificações em Rust e Zig

    • Segundo um material da Microsoft de 2019, cerca de 70% das vulnerabilidades tratadas a cada ano por atualizações de segurança eram problemas de segurança de memória
    • Dividindo a classificação de 2018 desse material sob a perspectiva de Rust e Zig, o quadro fica assim
    • 83,6% eram leituras/escritas fora dos limites, casts inseguros, leituras não inicializadas, stack overflow e problemas que não eram de segurança de memória — categorias que não são afetadas pela escolha entre Rust e Zig
    • 16,4% eram use-after-free, uma categoria que pode ser detectada pelo borrow checker do Rust, pelo Zig ReleaseSafe e por verificações no estilo Fil-C
    • O ReleaseSafe do Zig causa um pânico em tempo de execução quando memória já liberada é usada
      • A verificação é menos abrangente do que o subconjunto seguro do Rust
      • Há custo de execução e o programa pode entrar em pânico
    • O ReleaseFast omite as verificações em produção, mas as mantém em builds de debug e testes
    • Se todos os caminhos reais de execução forem testados, é possível obter uma segurança semelhante à de ReleaseSafe, mas essa cobertura de testes geralmente não é realista
  • Outros projetos em Zig e o unsafe do Rust

    • O TigerBeetle usa ReleaseSafe e, em uma rigorosa verificação com Jepsen, foram encontrados 2 bugs de segurança, mas nenhum deles tinha relação com segurança de memória
    • Ghostty e o compilador Zig, que usam ReleaseFast, também não têm CVEs causados por falta de segurança de memória no código Zig
    • Programas Rust também podem ter lacunas de segurança de memória por meio de unsafe dentro de dependências
    • Unsafe Rust tem riscos semelhantes aos do Zig em ReleaseFast, mas não há um recurso equivalente às verificações em tempo de execução do Zig que detectam problemas durante o desenvolvimento
    • Miri e Valgrind podem ajudar, mas não há muitos projetos Rust que os utilizem
    • Em vez disso, a cultura de usar unsafe raramente e revisá-lo com mais rigor é o que, na prática, constrói a forte reputação do Rust em segurança de memória
    • Também houve casos de vulnerabilidades relacionadas a unsafe em projetos baseados em Rust
      • O Deno teve CVEs de leitura fora dos limites e use-after-free
      • O Rocket teve um CVE de use-after-free
      • O Actix teve vários CVEs de falta de segurança de memória no período em que o uso de unsafe era anormalmente alto
    • Como, no Roc, a maior parte das alocações ocorre em arenas com tempos de vida simples, use-after-free não foi visto como um grande risco, e avaliou-se que as verificações adicionais do Zig seriam mais úteis para código inerentemente inseguro

Bugs de corrupção de memória confirmados após a reescrita

  • O resultado da classificação do issue tracker do Roc com Claude Opus 4.8 foi o seguinte
Compilador Houve corrupção de memória Sem corrupção de memória Total
Rust 21 2.575 2.596
Zig 10 421 431
  • Os 21 casos de corrupção de memória no compilador Rust não eram corrupção da lógica interna do compilador
    • O borrow checker cumpriu o papel pretendido
    • Eram bugs de compilação incorreta em que o código de máquina gerado erroneamente corrompia a memória no programa compilado
  • Dos 10 casos de corrupção de memória no compilador Zig, 8 também eram compilações incorretas
  • Os 2 restantes eram use-after-free no código de relatório de erros
    • Nas mensagens de erro de roc check e roc bundle, o nome do arquivo aparecia corrompido como o caractere de substituição U+FFFD
    • O borrow checker do Rust teria conseguido detectar ambos os bugs
  • O resultado que uma escolha diferente de ferramenta teria causado para usuários reais é o seguinte
Escolha de ferramenta Impacto real na segurança de memória
Zig ReleaseFast 2 bugs em que o nome do arquivo não era renderizado em algumas mensagens de erro
Zig ReleaseSafe 2 bugs em que alguns erros causavam pânico e não conseguiam renderizar o nome do arquivo
Rust borrow checker Prevenção de ambos os bugs
  • Considerando 18 meses, centenas de milhares de linhas e centenas de relatórios de bugs, a diferença entre as três opções não foi substancial para o projeto
  • O Bun lida ao mesmo tempo com valores JavaScript de garbage collection por rastreamento e valores gerenciados manualmente, de modo que use-after-free, double-free e vazamentos por falta de liberação tiveram grande peso
  • O compilador Roc não interage com JavaScript nem com outro garbage collector por rastreamento, portanto não sofre dos mesmos problemas de gerenciamento de tempo de vida
  • O Roc precisa mais de ferramentas para encontrar erros de memória no código de saída gerado do que erros internos de memória no compilador, e estes últimos estão fora do escopo do borrow checker

Resultados reais de tempo de build

  • zig build --watch -fincremental atualmente recompila alterações em cerca de 450 mil a 460 mil linhas de código Zig em aproximadamente 35 ms
  • A versão estável Zig 0.16.0 tem um bug que quebra -fincremental na base de código do Roc
    • A correção já foi incorporada, mas para usá-la é preciso migrar para uma pré-build do Zig 0.17.0 com quebra de compatibilidade
    • Como também seria necessário vendorizar as dependências relacionadas e atualizá-las para 0.17.0, decidiu-se aguardar a próxima versão estável
  • Os resultados medidos em um desktop Intel com Ubuntu 26 foram os seguintes
Compilador Roc Tamanho do código Build frio Build incremental
Original baseado em Rust 1.85.0 354K 32,4 s 10,0 s
Original baseado em Rust 1.97.0 354K 25,4 s 3,4 s
Zig 0.16.0 no ponto de paridade funcional 320K 39,6 s 8,6 s
Reescrita atual em Zig 0.17.0 464K 32,1 s 0,035 s
  • No ponto de paridade funcional, até artefatos que quase não mudavam eram reconstruídos sempre, mas agora eles só são gerados quando necessário
    • Com essa mudança, mesmo com o código cerca de 50% maior, o build frio ficou mais rápido
  • Ao passar de Rust 1.85.0 para 1.97.0, o build incremental caiu de 10 s para 3,4 s, uma melhora de cerca de dois terços ao longo de 18 meses
  • Os 35 ms do Zig equivalem a cerca de 1/100 de 3,4 s, e esse resultado vem de uma base de código cerca de 50% maior do que a usada na medição em Rust
  • Atualmente, -fincremental funciona apenas em x86-64, e muitos contribuidores usam Macs baseados em ARM, portanto ainda não é possível aproveitar totalmente a vantagem no tempo de build

Estruturas sem ponteiros e desserialização sem parsing

  • O novo cache em disco usa como formato em disco o mesmo layout de memória eficiente para execução, de modo semelhante ao que é usado no compilador Zig e no desenvolvimento de jogos
  • Todas as estruturas de dados do compilador são representadas por arrays de índices de 32 bits em vez de ponteiros, e em vários pontos usam o formato de arrays de structs
    • Reduz o uso de memória e aumenta a velocidade de execução
    • Permite gravar as estruturas de dados diretamente no disco, sem convertê-las para um formato de serialização separado
  • Ao desserializar, os bytes do disco não são analisados por parsing
    • Lê os bytes para a memória
    • Faz alguns realocamentos para que as estruturas de dados existentes apontem para os novos arrays
    • Depois disso, elas podem ser usadas imediatamente
  • A velocidade fica, na prática, limitada pela velocidade de I/O de leitura de bytes do disco para a memória
    • Se os dados estiverem no cache de disco do sistema operacional, os resultados do build anterior são carregados aproximadamente na velocidade de memcpy
  • roc check salva no disco, na primeira execução, os resultados de parsing, verificação de tipos etc.
    • Se o código-fonte de entrada não mudou, na segunda execução as estruturas de dados são movidas diretamente do disco para a memória
  • roc test também armazena em cache os resultados de testes determinísticos de funções puras
  • O cache funciona por arquivo; portanto, ao alterar um arquivo, apenas esse arquivo e seus arquivos dependentes são processados novamente
  • Essa abordagem é possível porque o compilador inteiro usa índices em vez de ponteiros; em estruturas comuns centradas em ponteiros, a desserialização sem parsing é impossível
  • Limites de segurança de estruturas baseadas em índices

    • Assim como um ponteiro pode apontar para um endereço incorreto, um índice também pode consultar o array errado e ler bytes arbitrários
    • O borrow checker do Rust trata do tempo de vida de ponteiros, mas não verifica a qual array um determinado índice pertence
    • Se a quantidade necessária de arrays for conhecida de antemão, o compact_arena do Rust pode criar tags de tipo por macros para impedir consultas ao array errado
    • Quando a quantidade de arrays varia conforme o número de módulos, como no Roc, essa técnica não pode ser aplicada; por isso o compact_arena também marca SmallArena::new como unsafe
    • Criar uma arena vazia por si só não gera perigo, mas o risco real está na indexação de arrays, que é realizada com muita frequência
    • Safe Rust é eficaz sob a premissa de que o unsafe é pequeno e isolado, mas, quando o unsafe é amplo como no Roc, essa premissa não se sustenta

O ecossistema Zig se encaixou bem no Roc

  • Para o Bun, por causa da interoperabilidade entre JavaScript e memória manual, o Drop do Rust, que executa código de limpeza apenas uma vez, foi útil
  • No Roc, ao contrário, a intenção era usar arenas separadas para cada módulo e etapa de compilação; por isso, pacotes Rust que pressupunham um alocador global e Drop implícito eram inconvenientes
  • No ecossistema Zig, APIs que recebem explicitamente o alocador são comuns, o que combina bem com o modelo de gerenciamento de memória do Roc
  • O ecossistema Rust era mais adequado à estrutura desejada pelo Bun, enquanto o ecossistema Zig era mais adequado à estrutura desejada pelo Roc
  • Reutilização do serializador de bitcode do LLVM

    • O LLVM é uma dependência central do otimizador do Roc e executa otimizações adicionais após as otimizações próprias do Roc
    • O LLVM frequentemente quebra a compatibilidade de suas principais APIs, então atualizar versões exigia tempo e custo consideráveis
    • O formato de bitcode serializado do LLVM é estável e retrocompatível; portanto, usar um serializador próprio permite escapar das mudanças na API C++
    • Para isso, é necessário um serializador de bitcode escrito manualmente e separado das bibliotecas C++ do LLVM
    • A implementação conhecida já existente estava no compilador Zig, e o novo compilador do Roc reutiliza esse código Zig
    • A maior fonte de dependências que o Roc obtém do ecossistema Zig é o próprio compilador Zig, mais do que pacotes comuns

Recursos do Rust que fazem falta no Zig

  • Na implementação do compilador, é necessário controle explícito de alocação, mas nos testes a alocação e liberação automáticas do Rust eram mais convenientes
    • O alocador de testes do Zig detecta vazamentos de memória e também consegue detectar vazamentos no código Roc compilado
    • Em contrapartida, em cada teste é preciso escrever corretamente init e defer deinit; se qualquer um deles estiver errado, o teste falha por vazamento
  • Embora o comptime do Zig se sobreponha ao polimorfismo paramétrico e ad hoc, esses dois tipos de polimorfismo fazem falta
    • O trait Allocator do Rust pode receber self
    • Implementações como o ArenaAllocator do Zig precisam receber um ponteiro anyopaque e depois fazer cast para seu próprio tipo
  • A ausência de campos privados em structs impede transformar em erro de compilação o acesso direto a campos que não deveriam ser acessados
    • Em revisões de diffs de código, aparece apenas o acesso ao campo, e não a documentação da struct original; por isso é preciso conferir separadamente a cada vez
  • Às vezes faz falta a consistência do Rust, em que funções, variáveis e constantes usam todas snake_case
  • unsafe e o borrow checker também tinham custos, mas davam a segurança de permitir se preocupar com certos problemas apenas dentro de blocos unsafe
    • Isso não significa defender que o Zig deva adicionar os mesmos recursos
  • No Zig, era comum descobrir código morto mais tarde do que no Rust
    • As ferramentas embutidas do Zig e o tidy.zig do TigerBeetle também não detectavam parte do código morto
    • Código morto não é gerado no binário e não afeta o usuário, mas é desfavorável para a manutenção da base de código
  • No Rust, upgrades de versões minoritárias e mudanças de edição foram, em geral, tranquilos
  • Atualmente o Zig não busca retrocompatibilidade, e isso já era uma condição esperada, então não foi um grande problema; ainda assim, a experiência simples de upgrade do Rust foi melhor

Pontos positivos do Zig

  • O Zig tem o apelo de um design subtrativo: reduz ferramentas familiares, como na programação funcional, mas obtém outras propriedades em troca
  • Não há macros, e muitos problemas, incluindo polimorfismo paramétrico, podem ser resolvidos com comptime ou funções comuns
  • É possível controlar o layout dos dados com precisão
    • Tipos inteiros que não são potências de 2, como u7 e u5, podem ser usados sem tratamento de bits separado
    • Há suporte nativo a packed struct
    • Funções podem ser inline no ponto de chamada, não no ponto de declaração
    • Recursos que no Rust exigiriam crates baseadas em macros podem ser usados sem dependências adicionais
  • A cadeia de ferramentas de build do Zig se mostrou adequada para gerar binários autônomos para Alpine Linux e WebAssembly
    • Também lida com builds especiais em que builtins, equivalentes à biblioteca padrão do Roc, são compilados como blobs binários opacos e incluídos no executável final
    • A Uber também usa a cadeia de ferramentas Zig na infraestrutura de build, mesmo sem usar a linguagem Zig
  • No tratamento de erros do Zig, falhas de alocação no heap também são tratadas como erros comuns em espaço de usuário
  • O Roc usa uma estratégia semelhante, em que erros se acumulam naturalmente por meio de tipos soma anônimos e payloads
  • Essa abordagem do Zig e do Roc é preferida em relação ao anyhow, ao thiserror e ao tratamento padrão baseado em Result do Rust
  • Para a sintaxe de propagação de erros, a preferência foi pelo ? pós-fixado do Rust em vez do try do Zig, e por isso o Roc também adotou o operador ? pós-fixado
  • Considerando o projeto como um todo, incluindo APIs de alocadores granulares e código reutilizável para compiladores de alto desempenho, a escolha pelo Zig foi muito satisfatória

Próximos passos do Roc

  • A versão 0.1.0 do novo compilador está planejada para o segundo semestre deste ano e será o primeiro lançamento numerado do Roc
  • Mesmo antes do lançamento, é possível testar os builds Nightly, mas atualmente ainda há vários bugs, recursos incompletos e documentação inacabada
  • A Roc Programming Language Foundation é uma organização sem fins lucrativos 501(c)(3) dos EUA, e as doações são usadas principalmente para remunerar contribuidores
  • O andamento do desenvolvimento e perguntas futuras podem ser acompanhados no Roc Zulip

2 comentários

 
GN⁺ 3 시간 전
Opiniões no Lobste.rs
  • Em termos numéricos, 35 ms no Zig é quase 100 vezes mais rápido que 3,4 s no Rust, e o código também é cerca de 50% maior, mas na prática de desenvolvimento essa diferença parece um pouco exagerada
    Se você não recompila o compilador a cada minuto, mas sim a cada 10 minutos, o tempo economizado é de cerca de 3 segundos, e 3,4 s já é rápido o bastante
    Conforme a base de código cresce, o Rust pode ficar mais lento, mas o tempo de build frio na verdade piorou, e somando isso ao fato de trocar de notebook a cada 2 anos e às melhorias no próprio compilador, é difícil afirmar que o tempo de build do Rust continuará aumentando no longo prazo

    • Ver erros de compilação e resultados de testes invalidados aparecerem antes mesmo de apertar a próxima tecla ou mover o cursor logo após salvar é uma vantagem enorme
      O modo check do Rust e o LSP são mais rápidos que uma recompilação completa, mas não chegam ao mesmo nível
    • Assumir que a recompilação acontece só uma vez a cada 10 minutos acaba subestimando a diferença
      Usando a combinação --watch -fincremental, dá para recompilar a cada salvamento e receber feedback de forma muito rápida e frequente
      Já migrei um projeto pessoal de Rust para Zig e também usei Rust por anos no trabalho, e a velocidade de compilação do Zig foi realmente uma mudança que deu um grande alívio
  • 35 ms é incrivelmente rápido
    Só relincar já parece que levaria mais do que isso, então fico curioso sobre o que o compilador realmente está fazendo

    • O Zig tem um linker próprio
    • O ponto central é o link incremental e a forma de modificar o binário existente no lugar; se bem me lembro, no compilador Roc isso era chamado de surgical linking
      Se você muda a implementação de uma função, basta inserir o novo assembly no binário existente, mas inlining, mudanças na assinatura da função, dependências e realocação quando falta espaço exigem análise adicional
    • A equipe do Zig cuidou de grande parte do trabalho de compilação incremental, e o novo linker ELF ainda não está concluído, mas foi projetado com o objetivo de fazer link incremental no nível de funções individuais e variáveis globais
      Ao modificar ou adicionar uma função, ele encontra uma área livre grande o suficiente na seção .text do executável de saída e grava ali o novo código de máquina
      Se faltar espaço, talvez seja preciso expandir a .text e mover outros dados, mas as seções são ampliadas de forma exponencial para diluir esse custo
      Quando necessário, ele também adiciona a tabela de símbolos e entradas de realocação, deixando espaço livre antecipadamente para elas, então basta fechar o arquivo ao terminar o trabalho
      Com base em um resultado do Tracy que vi recentemente, estimo que, dos 35 ms totais, o linker tenha usado cerca de 1 ms
  • O número de cerca de 1.200 usos de unsafe em 300 mil linhas de código Rust é maior do que eu esperava
    Por exemplo, o Inko tem apenas 162 expressões unsafe { ... } e 87 funções unsafe expostas ao código gerado via ABI C, e todo o código Rust soma cerca de 88 mil linhas
    Ainda assim, como todo o backend LLVM é na prática uma enorme área unsafe por causa de como o Inkwell funciona, é difícil tirar uma conclusão com uma comparação simples
    Eu marco como unsafe apenas funções que realmente comprometem a segurança de memória, como mexer diretamente com ponteiros, mas outros desenvolvedores também usam isso como dica de que certa operação precisa ser feita antes para evitar pânico

  • O número de 40 mil ocorrências de unsafe na biblioteca padrão e no compilador Rust está incorreto
    Esse total contou até a palavra aparecendo em testes e comentários; a maior parte está na biblioteca padrão, e no compilador em si, mesmo incluindo comentários e testes, há menos de 2 mil ocorrências
    Durante o tempo em que contribuí com o rustc, menos de 1% dos meus PRs continham unsafe, a ponto de código inseguro dentro do compilador ser raro
    É natural que haja muito unsafe na biblioteca padrão, que implementa o runtime básico do qual todo Rust depende, e esse tipo de código é inseguro em qualquer linguagem, de forma explícita ou implícita
    Esse compilador é cerca de 10 vezes menor que o rustc, mas usa uma quantidade parecida de unsafe, então talvez não dê para dizer que isso é generalizado, mas ainda assim aparece com frequência, e fico curioso sobre por que ele precisa de muito mais unsafe do que o rustc

    • Nossa base de código também mistura compilador e biblioteca padrão, e partes da biblioteca padrão são implementadas como funções intrínsecas do compilador, então considero que essa métrica comparou objetos semelhantes
      Não separamos a análise do unsafe relacionado à biblioteca padrão em nenhuma das duas bases, mas, se tivéssemos implementado o novo compilador em Rust, espero que ele tivesse mais unsafe que o rustc por causa do cache e da estrutura que usa índices em vez de ponteiros
      A intenção não era diminuir o trabalho da equipe do Rust, mas explicar nossas escolhas e o andamento do projeto, respeitando os resultados de outros projetos
  • É um texto que avalia de forma equilibrada, sem esconder os prós e contras de Rust e Zig, e foi especialmente honesto ao concluir que, mesmo depois de 18 meses escrevendo centenas de milhares de linhas e lidando com centenas de bugs, o resultado do projeto provavelmente não teria mudado muito se tivessem escolhido outra linguagem entre as opções
    Ainda assim, é difícil entender a explicação de que, em compiladores que geram código de máquina como o Roc ou o rustc, operações de memória inseguras representam uma grande parte do trabalho
    Há muitos compiladores escritos em OCaml ou Haskell, e gerar código de máquina em si é apenas montar bytes em um vetor e gravá-los em um arquivo, então não há motivo para isso ser inseguro
    Se fosse interpretação ou compilação JIT, eu entenderia, mas fico curioso sobre por que isso seria necessário também na compilação comum

    • Gerar código de máquina em si é apenas gravar bytes, então dizer que isso é inseguro foi uma formulação imprecisa
      O risco surge ao executar o código de máquina gerado, e como compiladores reais muitas vezes geram e executam código de máquina ao mesmo tempo, foi dito que isso representa uma grande parte do trabalho
      Além de interpretação e JIT, isso inclui avaliar código do usuário em tempo de compilação, como em const fn do Rust ou expressões que podem ser elevadas ao topo no Roc, ou executar testes e então inspecionar a saída para decidir o que mostrar ao usuário
    • Parece querer dizer que, para criar um compilador incremental muito rápido como o compilador Zig, é preciso uma linguagem de baixo nível e uma quantidade considerável de operações inseguras
    • No texto, casos em que um bug de segurança de memória surge por compilação incorreta também são contados como bugs de segurança de memória do compilador
      Seja a memória corrompida no processo do compilador ou no programa gerado, o critério é que a causa do comportamento incorreto do processador foram instruções produzidas pelo compilador, e o lugar a ser corrigido também é o código do compilador
    • O Roc já usava Zig no runtime e na biblioteca padrão quando o compilador ainda era escrito em Rust
      Componentes como código de ligação externa e o coletor de lixo também exigem muito unsafe em Rust, o que anula em boa parte as vantagens do Rust seguro
      Dá para escrever só o compilador em uma linguagem segura, mas desempenho é um problema; tanto o Zig quanto o compilador do Roc usam bastante estrutura de arrays (SoA) e índices de arrays em vez de ponteiros
      Implementar isso em Rust acaba contornando o borrow checker e fazendo perder as vantagens de segurança relacionadas
      As relações de tempo de vida no compilador são surpreendentemente simples: em uma fase, aloca-se, cria-se e modifica-se dados em uma arena; na fase seguinte, eles são passados como somente leitura; depois disso, pode-se descartar a arena inteira
      As partes mais complexas são a compilação incremental e o linking, que leem estado do disco e modificam binários no lugar; nesse processo, pode haver corrupção de estado, bugs, compilação incorreta e problemas de memória, mas isso é separado da segurança de memória do próprio processo do compilador
      Segurança e correção são conceitos muito mais amplos do que a segurança de memória dentro do programa, especialmente quando se quer executar de forma segura e correta tarefas que são difíceis de expressar em Rust
  • Fiquei feliz em ver a menção a mim como criador do compact_arena, mas a forma como o uso de unsafe nessa biblioteca foi descrito está errada
    O objetivo do compact_arena é garantir que não seja possível usar new incorretamente em Rust seguro, tornando a indexação segura
    A macro mk_arena pode ser chamada em código Rust seguro mesmo dentro de loops em que não se sabe quantas arenas serão necessárias

    • Posso não ter entendido a explicação de que new é usado com segurança, mas na seção de segurança de https://docs.rs/compact_arena/0.5.0/… está escrito que a tag passada ao construtor é a base da técnica de indexação, e que usá-la em outra arena pode misturar índices de duas arenas, levando a acesso fora dos limites e comportamento indefinido
      Meu ponto não era a chamada a new() em si, mas que o risco surge ao indexar valores criados por new(), e considero que o lugar a ser auditado e marcado como unsafe seria a chamada de indexação em si
      Mas é possível que eu tenha entendido o design de forma errada
  • Pelo que sei, em Python é preciso reiniciar o programa para refletir código novo, e hot reloading exige extensões não padronizadas e escrita cuidadosa do código
    Hot reloading é mais comum em ambientes de desenvolvimento baseados em imagem, como Lisp ou Smalltalk, ou em Erlang

    • Com importlib.reload(), hot reloading também é possível em Python, mas não sei o quão amplamente isso é usado
  • Fico curioso se deixar de usar cargo build pode reduzir o tempo de build
    Não está claro se o trabalho de configurar o Bazel seria maior ou menor do que portar para Zig, e claro que o tempo de build não foi o único motivo para a troca de linguagem

 
GN⁺ 4 시간 전
Comentários do Hacker News
  • No geral, o texto é bom, mas é difícil concordar com a afirmação de que, em compiladores que geram código de máquina como Roc ou rustc, operações inseguras de memória seriam uma grande parte do trabalho
    Patching de binário em execução ou recarregamento de código talvez precisem de código inseguro, mas no processo normal de gerar um executável não há motivo para que a própria geração de código de máquina seja insegura
    Na verdade, é mais provável encontrar código inseguro no runtime da linguagem

    • Concordo. Gerar código de máquina é só escrever bytes, e a insegurança potencial aparece quando esse código de máquina é executado
      Na prática, porém, muitos compiladores também executam diretamente o código de máquina enquanto o geram, então usei a expressão “uma grande parte do trabalho”, mas o compilador não precisa necessariamente fazer as duas coisas
      Eu tinha em mente não só patching de binário, recarregamento de código e runtime, mas também avaliar código do usuário em tempo de compilação, como const fn no Rust ou expressões que podem ser içadas para o topo no Roc, além de executar testes e inspecionar sua saída para decidir o que exibir
    • Muitas vezes se distorce o fato de que até linguagens com segurança de memória têm blocos de código inseguro para concluir: “então de que adianta segurança de memória?”
      É uma lógica parecida com dizer que, como ainda é possível bater a cabeça usando cinto de segurança, o cinto é só um incômodo e não deveria ser usado
    • Gerar código de máquina não exige inerentemente operações inseguras, mas acho que isso reflete prioridades diferentes
      Em código de altíssimo desempenho, mudam desde as estruturas de dados e algoritmos até a estratégia de alocação de memória, e o TigerBeetle é famoso por alocar toda a memória de uma vez na inicialização
      Como o compilador do Roc também pretende adotar concessões semelhantes às do Zig, é natural que apareçam vários padrões em comum
    • Dizer que gerar código de máquina requer operações inseguras está claramente errado. Um compilador pode trabalhar apenas com estruturas de dados totalmente abstraídas, e se há algum lugar em que operações inseguras de memória seriam mais naturalmente necessárias, isso está mais para o linker
    • É difícil entender em que parte de um compilador seriam necessárias operações de memória inseguras para realmente produzir código de máquina
  • Não parece haver base para a alegação de que ReleaseSafe pega use-after-free com verificações em tempo de execução
    Procurei na documentação do Zig sobre verificações de segurança de memória em runtime, mas não encontrei nada relevante buscando por use-after-free, UaF ou safety-checked, e mesmo usando DebugAllocator em builds de release isso não detecta o problema de forma confiável
    O assunto está resumido em https://landaire.net/memory-safety-by-default-is-non-negotia...

    • Como alguém que já escreveu bastante código em Zig, mesmo que esse recurso exista, ele nunca funcionou direito no meu código
      É uma avaliação formada escrevendo código diretamente, antes da IA; talvez algo mude na combinação de Zig com LLMs, mas quanto mais leio o texto, mais vejo afirmações estranhamente formuladas, o que dificulta confiar nele
      Soa menos como um julgamento técnico sincero e mais como um texto escrito para justificar uma discussão já existente, mas gosto de textos e linguagens incomuns e tenho certa antipatia pela empolgação excessiva com IA, então por enquanto vou presumir boa-fé
    • Pelo que sei, ReleaseSafe basicamente adiciona verificações de limites e faz panic em código inalcançável
      O Zig aparentemente não oferece segurança temporal de memória (temporal memory safety)
  • É interessante que o OCaml, já maduro, tenha sido usado para validar protótipos por ser flexível e suficientemente expressivo, mas não tenha sido escolhido como linguagem da implementação final
    Também fica a dúvida se o build incremental do Zig é de fato significativamente mais rápido que o dune, e “por que Zig?” nem sequer abordou a vantagem da compilação cruzada
    Gostaria de entender se controle fino de memória é realmente tão importante em um compilador e em que marco mantenedores decidem migrar para outra linguagem, como aconteceu com Rust e WASM, que começaram em OCaml

    • O Rust saiu do OCaml ao decidir fazer uma reescrita do compilador em Rust. O texto também sugere isso como um momento comum para reescrever um projeto inteiro, e concordo com essa avaliação
    • Um dos objetivos centrais do Roc é a velocidade de compilação. Suponho que o OCaml tenha sido descartado como candidato por não ser uma linguagem de programação de sistemas
  • O build incremental do Zig é claramente um recurso decisivo, e dá para entender a escolha de trocar de linguagem para obter isso no curto prazo
    Mas, no médio prazo, espero que o Rust também ganhe algo parecido em um futuro próximo
    Quero velocidade, mas não quero acelerar tanto a ponto de me prejudicar; estou criando minha própria linguagem para combinar a segurança do Rust, os recursos do Zig e um runtime de Go sem garbage collector

    • Builds rápidos de Rust já estão em andamento em https://rust-lang.github.io/rust-project-goals/2026/roadmap-..., e a maior parte das metas dessa página é para este ano
    • É bem mais provável que o tempo de compilação do Rust melhore antes que o Zig se torne muito mais seguro
    • Em vez de esperar o compilador Rust ficar mais rápido, será que não valeria adicionar algum tipo de borrow checker ao Zig?
      Isso parece mais viável, e talvez até dê para implementar em espaço de usuário
    • Tirando o garbage collector, fico curioso sobre qual seria a vantagem especial do runtime do Go
    • De vez em quando uso tokens restantes do Codex em https://github.com/ityonemo/clr na tentativa de combinar a segurança do Rust com os recursos do Zig
  • Teria sido muito mais convincente se tivesse havido uma comparação científica de fato no processo de escolha da linguagem de implementação do compilador
    Parece que partiram da suposição não verificada de que um compilador de alto desempenho precisa de uma linguagem de sistemas de baixo nível (https://www.roc-lang.org/faq#self-hosted-compiler) e concluíram que, fora Rust, a única opção era Zig
    O desempenho de compiladores é dominado por algoritmos, e até linguagens gerenciadas rápidas, usando os mesmos algoritmos, em geral ficam com tempo de execução dentro de no máximo o dobro, enquanto a diferença de desempenho causada por algoritmos não tem um limite comparável
    O próprio Zig é um contraexemplo à teoria de que escrever um compilador em uma linguagem de baixo nível o tornaria mais rápido, e as cerca de 15 mil linhas por segundo do Roc não são rápidas. Há material mostrando que um compilador ML já processava 3 mil linhas por segundo em 1998 (https://flint.cs.yale.edu/cs421/case-for-ml.html)
    Talvez fosse mais útil para o futuro parar o trabalho atual no compilador e criar um compilador self-hosted para um subconjunto pequeno de Roc, possível em menos de 10 mil linhas
    Assim, em vez de uma implementação com 300 mil linhas, seria possível testar várias implementações na escala de 10 mil linhas e verificar se uma linguagem de baixo nível é realmente necessária para atingir as metas de desempenho
    No processo de self-hosting, as funcionalidades de Roc que realmente importam apareceriam, mais código em Roc seria escrito e, ao melhorar os recursos de uso geral necessários ao compilador, as aplicações abaixo dele também melhorariam

    • Exigir ciência aqui também soa categórico, e esta avaliação igualmente não tem muitas evidências empíricas
      Só o fato de ML compilar rápido nos anos 1990 não basta para julgar a velocidade de compilação do Roc hoje. O design da linguagem impõe restrições fortes aos algoritmos necessários, e o hardware moderno também é muito mais complexo
      O Roc parece ter algum grau de sobrecarga e algoritmos sofisticados para evitar alocar closures no heap, e essas exigências podem criar uma complexidade algorítmica inevitável
      Quando se chega ao limite da otimização algorítmica, o que resta é reduzir fatores constantes e, especialmente em linguagens de alto nível com gerenciamento de memória, existe claramente um limite inferior para o quanto isso pode ser reduzido
      Já vi casos em código real em que controlar diretamente o layout de memória trouxe melhoria de mais de 10x no desempenho, e na indústria de games esse tipo de trabalho às vezes ocupa grande parte da carreira. A ideia de que um algoritmo esperto resolve todos os problemas de desempenho está longe da realidade
  • Builds de Rust são uma das grandes causas de desperdício de espaço em disco em praticamente qualquer computador, e ao compilar várias bibliotecas é fácil acumular dezenas de GB
    Dá para configurar uma pasta global de build para reutilizar dependências entre projetos, mas seria melhor que qualquer solução desse tipo viesse como comportamento padrão

    • É uma estrutura que troca espaço em disco por builds mais rápidos, e em alguns casos usar ainda mais espaço pode aumentar ainda mais a velocidade
      Ainda assim, coleta de lixo de cache é necessária, e está na fase final um novo trabalho de reorganização dos artefatos intermediários de build para facilitar isso
    • É um contraste curioso se pensarmos em como o tamanho de node_modules é sempre criticado no ecossistema JavaScript
      Mesmo em um projeto Tauri onde o código de backend é muito menor que o de frontend, os artefatos de build de Rust chegam a 9 GB, enquanto node_modules fica em apenas 550 MB
  • Entendo Roc como uma linguagem de script embutida em uma C ABI, e tenho curiosidade sobre os casos de uso reais
    Fico pensando se a ideia é competir com WASM em ambientes de plugin que oferecem uma grande plataforma Roc e por que um desenvolvedor de aplicações teria motivo para expor uma camada Roc em vez de usar WASM, que permite aos autores de plugins usar qualquer linguagem
    Se for uma linguagem no nível da aplicação usando uma pequena plataforma Roc, também tenho curiosidade se a proposta é competir com Gleam no código HTTP do lado do servidor e com Elm no código cliente

    • Gosto especialmente de descobrir em que tipo de aplicação uma linguagem desconhecida, em especial uma funcional, é usada, mas mesmo lendo o roc-lang.org e o FAQ não consegui entender os usos concretos do Roc
  • Tempo de compilação é um fator muito subestimado. Ter de esperar 10 minutos por um build de C++ é minha maior reclamação, e isso quebra completamente o fluxo de desenvolvimento

    • No VS Code, uso o Rust-Analyzer para análise sintática e autocompletar, mas ele fica lento demais porque executa o compilador ao salvar o arquivo para atualizar as informações
      A diferença ao trocar de um arquivo .rs para um .ts é tão grande que parece que troquei o próprio computador
  • Não uso Zig diretamente, mas tenho grandes expectativas para algumas possibilidades
    Um novo jogo escrito em Zig, software distribuído em que TigerBeetle já mostrou potencial e a área de robótica, que pessoalmente me interessa, são os pontos que mais me animam