Visão geral e filosofia
Uma linguagem de sistemas compilada, focada em:
- Alta disponibilidade e resiliência de falhas (inspiração BEAM/Erlang)
- Controle granular de memória sem o custo cognitivo do Rust
- Concorrência first-class simples e legível (inspiração Go)
- Explicitude em tudo: erros, memória, comunicação, ownership
- Simplicidade da especificação: Go e Zig provam que spec simples = linguagem adotável
A linguagem não roda na BEAM VM. O compilador é shippado com um runtime próprio que implementa isolamento de processos, supervisão e fault tolerance nativamente.
Inspirações:
| Feature | Inspiração |
|---|---|
| Fault tolerance / isolamento de processos | BEAM/Erlang |
| Concorrência e channels | Go |
| Expressividade do type system | Rust (sem lifetimes) |
| Async/IO, comptime, suporte a inteiros | Zig |
| Dispatch por tipo | Odin |
| Simplicidade geral | Go + Zig |
O teste do opt-in (o critério anti-Rust-2)
Seção intitulada “O teste do opt-in (o critério anti-Rust-2)”A linguagem é sofisticada onde precisa (comptime, contratos, gerência estratégica de memória), e a pergunta natural é como isso não vira um “Rust 2”. A resposta é um critério aplicado a todo conceito novo. O que torna Rust caro não é o número de conceitos. É que eles vazam pra todo programa, o tempo todo: lifetimes infectam toda assinatura, async colore tudo, o borrow checker opina em cada linha. São inescapáveis e onipresentes.
O teste do opt-in: um conceito custa caro se o programador que não o usa ainda precisa entendê-lo para ler código alheio ou escrever o dele. Conceito que você pode ignorar até precisar, e que não aparece no código de quem não o invoca, é barato, não importa quão sofisticado seja por baixo. É por isso que unsafe, assert/assume, @requires e as pós-condições (seções 5 e 14) podem existir sem tornar a linguagem um Rust 2: são todos opt-in e nenhum vaza. Quem escreve código safe nunca os encontra; eles aparecem só quando você desce ao metal, e ficam invisíveis até lá. O teste separa “sofisticação que você convoca” de “complexidade que te persegue”.
Uma ressalva honesta ao próprio teste: ele mede o que vaza pro código de quem não usa, não o que vaza pro modelo mental. Você nunca escreve @mm, mas pra ler qualquer código precisa saber que ponteiro só nasce de var no heap, que imutável aliasa de graça, que mut não escapa. É semântica de memória no caminho default, sem anotação, que todo programa carrega em silêncio. Alguma teoria de cada sistema (memória, processos) sempre vaza pro modelo mental: é o piso inevitável, distinto do vazamento-pro-código que o teste de fato previne. O teste continua valendo; só não é a história toda.
Uma distinção honesta também: o opt-in reduz custo, não tamanho. A especificação é pequena e consistente (poucas keywords, construtos unificados, uma regra por símbolo); a linguagem é grande, com muita capacidade (comptime, contratos, gerência estratégica, FFI, extensões). O que o opt-in garante não é que a linguagem seja simples. É que seja consistente, e que o que você não usa não te custe. Vender “simples” seria mentira; o que se vende é “consistente, e você só paga pelo que convoca”. E parte dessa honestidade é nomear as fricções aceitas em vez de descobri-las depois: o catch não tem um default curto que descarta o erro em silêncio (você lida ou propaga, verboso de propósito, seção 7); o decl custa um pouco no skim rápido frente a um struct/enum dedicado (o tipo do construto vem do corpo, não da palavra, mitigado por tooling, seção 2). São preços escolhidos, não acidentes.
O princípio do não-envenenamento (namespace mínimo)
Seção intitulada “O princípio do não-envenenamento (namespace mínimo)”Irmão do teste do opt-in, um plano abaixo. O teste do opt-in protege quem não usa uma feature; o não-envenenamento protege quem não usa um nome. Tudo que vive no escopo global (toda keyword, todo tipo builtin, toda função sempre-disponível) é imposto a todo programa, inclusive aos que nunca o invocam: é custo cognitivo e ruído de autocomplete/LSP para todos. Logo o escopo global contém só o inevitável: o que está tecido na gramática (keywords) e os tipos que a sintaxe core pressupõe (Result, Optional, Channel, os primitivos). Tudo que pode ser importado, é importado, funções inclusive: panic vive em runtime, print em io, e uma lib de cálculo puro nunca os vê. Não há exceções “fundamentais demais pra importar”. A exceção no core é falha de design, e um core bem pensado quase não as tem (é parte do que adoeceu linguagens que misturaram um punhado de builtins com “todo o resto é import”). É o teste do opt-in levado ao namespace.
O core é desenhado completo e estabiliza devagar. A linguagem é projetada inteira antes de 1.0 justamente pra não repetir o erro de adicionar peças centrais tarde, quando uma linguagem restringe demais sua própria filosofia e aí algo novo (genéricos chegando anos depois, por exemplo) entra poluindo, porque o resto foi desenhado assumindo que aquilo nunca existiria. Aqui o core nasce fechado; pós-1.0, a linguagem cresce em bibliotecas, não em sintaxe. O core muda pouco e devagar, e quando muda de forma incompatível, o migration tool (seção 22) aplica a correção mecânica. Isto pareia com o teste do opt-in: os dois protegem a coerência da linguagem ao longo do tempo, um no espaço (features não vazam), outro no tempo (o core não incha).
Sistemas e fronteiras
Seção intitulada “Sistemas e fronteiras”Dizemos “linguagem de sistemas” o tempo todo e quase nunca definimos o que é um sistema. A palavra é larga: sistema pode ser o operacional (kernel, userspace), pode ser a aplicação (backend, frontend, banco), pode ser um sistema de IA. Cada linguagem que se diz “de sistemas” responde a essa pergunta de um jeito, quase sempre de um jeito implícito, sem nunca enunciar. Então a pergunta vem antes do rótulo: o que é um sistema? Só depois de respondê-la é que “linguagem de sistemas” quer dizer alguma coisa, e a resposta é de cada um.
A nossa é deliberada, e é justamente a coisa que costuma passar batido: uma linguagem é, ela própria, composta de vários sistemas. Memória (MemorySource/MemoryManager, seção 5), concorrência (supervisão, processos, seção 3), execução (async / sync, seção 12), o sistema de tipos, o FFI com C (seção 17), o FFI com assembly (seção 18), e a UI, que é a primeira extensão de compilador (seção 20). Cada um é separado e contido, um sistema à parte. Somos uma linguagem de sistemas no sentido de que tratamos os nossos próprios sistemas como separados.
E cada sistema tem suas cores. A execução tem duas (async, sync); o de tipos tem duas (genérico, não-genérico); a memória tem várias (a fonte, a estratégia). Tainted coloring é quando as cores de um sistema vazam para outro. É o que acontece com async em quase toda linguagem: as cores da execução mancham as do sistema de tipos. Passam a existir tipos-async e tipos-não-async, além de genéricos e não-genéricos, e o sistema de tipos carrega uma carga que era da execução. É exatamente isto que o teste do opt-in e o não-envenenamento vinham protegendo, dito agora por inteiro: cada sistema carrega só as suas cores. Você só vê as cores de um sistema quando interage com aquele sistema, não antes, não de raspão.
Por isso somos uma linguagem de fronteiras, e por isso ela aparece na filosofia de interfaces (seção 9): quando um sistema toca outro, a barreira é neutra, não carrega cor. As interfaces de memória não dizem qual memória; as de async/IO não dizem como se suspende. É o que deixa UI e FFI de baixo nível coexistirem sem se conhecer: são sistemas que não se tocam, e quando tocam, tocam por uma fronteira limpa.
As duas falhas que evitamos são opostas. Rust vaza. Vários sistemas escorrem uns nos outros, e a conta chega na leitura: porque a linguagem é toda tainted, a carga cognitiva é alta logo de cara. O começo do Rust book é um varal de “veremos mais à frente”, e você segue com um punhado de conceitos mal-explicados na memória, desmistificando cada um só lá pro fim. Não é uma linguagem em que você compõe conhecimento; é uma em que você desfaz a confusão que já acumulou. Cansa, e é a nossa resposta a “por que não um Rust 2”. Go funde. Trata a linguagem como um único “sistema simples” em vez de vários subsistemas (a filosofia de ser simples ao ponto de o programador menos experiente já produzir), e o preço é a rigidez: estender (genéricos, generators) obriga a mexer na linguagem inteira, porque o que devia ser um sistema esbarra na concorrência e na execução, que nunca estiveram isoladas dele.
O core é a raiz de onde os subsistemas saem, e é por isso que lutamos pra mantê-lo mínimo: uma adição ao core (uma keyword, um tipo primitivo) impacta todos os sistemas de uma vez. E a disciplina de fronteiras não é decoração: é o que permite atualizar um sistema (a concorrência, digamos) sem sacudir os outros, e o que deixaria um sistema novo, no futuro, entrar sem reescrever os que já existem.
Unificação por conceito
Seção intitulada “Unificação por conceito”Quando várias keywords nomeiam o mesmo conceito e diferem só no objeto sobre o qual o conceito é exercido, elas viram uma keyword (o conceito) e o objeto desambigua. match/switch/select têm todos a mesma forma, keyword {objeto} { casos }, e o mesmo ato, comutar: match comuta estrutura, switch comuta valores, select comuta saídas. O que muda é quem é o objeto e como ele se escreve. Então é uma keyword só, match, e o objeto (ou sua ausência, no select) diz o resto. O mesmo valeu pra for/while/loop (um loop, a condição decide) e pra struct/enum/interface (um decl, o corpo decide, seção 2); e o mesmo movimento, nos decoradores, fundiu @register/@register_group e as diretivas de asm em @register e @asm.
O critério é estreito, e errá-lo quebra a linguagem nos dois sentidos. “Mesmo conceito” quer dizer mesmo ato e mesmo sistema, não mesma forma. A forma engana nas duas pontas: struct, enum e interface têm corpos de formato diferente e mesmo assim fundem, porque são o mesmo ato (declarar um tipo) no mesmo sistema (o de tipos); union tem corpo idêntico ao do produto e mesmo assim não funde, porque é outro ato (reinterpretar bytes) em outro sistema (o unsafe). Unir nunca atravessa a fronteira entre dois sistemas: isso seria, de novo, uma cor manchando outra.
E há o engano oposto, que parece unificação e não é. let/var/const, pub/priv, @pin/@unpin parecem famílias prontas pra fundir: pequenas, vizinhas, do mesmo domínio. Mas não são N nomes pra um conceito; são um eixo com um default. let/var/const é o eixo da mutabilidade; pub/priv, o da visibilidade; @pin/@unpin, o do pinning. Fundir um eixo não simplifica: apaga a distinção que ele carrega. Daí as duas saídas do teste: se é o mesmo ato com objetos diferentes, unifique e deixe o objeto desambiguar; se é um ato com um eixo de modos, preserve o eixo e deixe os nomes marcarem as posições.
Explicitude por marcação, não por verbosidade
Seção intitulada “Explicitude por marcação, não por verbosidade”Explicitude e ergonomia parecem um cabo-de-guerra (mais de um, menos do outro), mas não dividem a mesma balança. Explicitude é marcar a intenção sem ambiguidade; verbosidade é quantos caracteres. Um glifo curto marca a intenção tão bem quanto uma keyword longa, desde que carregue exatamente uma intenção. Por isso a linguagem usa símbolos terso e explícitos: _ é “ausência” (uma intenção só, em _ :=, _ =>, timeout(_)); e onde há duas intenções há dois símbolos: assert (“checo a segurança”) e assume (“assumo a segurança”), nunca uma keyword só ora checando ora não (seção 5). O teste não é “palavra vs símbolo”, é: este símbolo carrega uma intenção, sem ambiguidade? Se sim, é curto e explícito ao mesmo tempo.
É disso que sai a forma exata do não-retorno-implícito (seção 14): o que se proíbe não é “deixar de escrever return”, é o retorno por posição, ter que raciocinar “esta expressão é o retorno porque é a última”. Os marcadores de saída são todos explícitos, e nem todos são a keyword return: o return (palavra), o ?: do ternário e o => do lambda (glifos) marcam a saída sem depender de posição. Entregar-por-glifo é tão explícito quanto entregar-por-palavra; o proibido é entregar por posição, e isso não acontece em lugar nenhum.
A balança entre explícito e ergonômico, então, não se paga sacrificando um pelo outro: paga-se exigindo que cada símbolo tenha uma intenção, sem ambiguidade. A terseza vem de bons símbolos; a explicitude vem de cada símbolo significar uma coisa só.
Disciplina de decoradores
Seção intitulada “Disciplina de decoradores”O @ é a etiqueta de um subsistema (@mm = memória, @supervisor = concorrência, @cimport = FFI), não uma anotação conveniente para qualquer feature. Daí a regra que contém a proliferação: antes de adicionar um decorador ao core, a pergunta é “isto é mesmo um subsistema com fronteira, ou uma feature vestida de anotação?”. Se não é um subsistema, não ganha decorador de core: vira keyword (se é conceito do núcleo), construto, ou função comum. O conjunto de decoradores do core fica curado e pequeno por construção: cada um marca uma fronteira real.
E o crescimento de decoradores específicos de domínio não acontece no core; acontece nas extensões (seção 20). A UI traz @state/@component; um framework futuro traria os seus; cada um vem da extensão que o define e o realiza, declarando suas capabilities. A defesa contra a explosão é então dupla: o core filtra (só subsistema vira decorador de core) e a expansão mora em extensões (opt-in, capability-declared, fora do core), não em acréscimo ao core. Você nunca encara todos de uma vez: encara os do subsistema em que está, ou os da extensão que ligou.
As camadas de estabilidade (os tiers)
Seção intitulada “As camadas de estabilidade (os tiers)”A linguagem já afirmou que o core nasce fechado e estabiliza devagar e que crescemos em bibliotecas, não em sintaxe (não-envenenamento, acima). Isso esconde uma pergunta que vale enunciar inteira: se nem tudo muda na mesma velocidade, o que decide a velocidade? A resposta organiza o ecossistema todo em camadas de estabilidade (os tiers), e o eixo que as separa não é a idade nem a área: é quão livremente se pode mexer no contrato. O tempo é só o sintoma (o que tem contrato intocável tende a durar décadas, o que tem contrato que gira dura anos), mas a régua é o contrato. Cada tier é uma promessa diferente sobre o que pode mudar, e sobre quem é dono da mudança.
Tier 0, o core. O chão: os padrões e algoritmos que a ciência da computação e a engenharia de sistemas não mudam há meio século. Floats IEEE, inteiros, o protocolo TCP/IP, o algoritmo de uma lista linkada, a codificação UTF-8, coisas cujo contrato é um padrão consolidado do campo, não uma escolha nossa. Não se revisa o IEEE 754 nem se reescreve o que “lista linkada” significa; o que se faz é adicionar uma estrutura nova, um protocolo novo, jamais editar o que já está fincado. Você não muda o TCP/IP: cria um protocolo novo ao lado. Por isso o tier 0 é add-only: ele não muda porque o que guarda, por natureza, não muda. E a própria linguagem é o bedrock deste tier: a disciplina de editions é o add-only levado à sintaxe. Acrescenta-se forma, essencialmente nunca se quebra a antiga, e quando o incompatível é inevitável o migration tool (seção 22) aplica a correção mecânica.
Mas “add-only” é sobre o contrato observável, não o código-fonte. Um bugfix que faz a implementação passar a bater com o contrato já prometido é permitido; sem isso não daria pra corrigir uma falha de segurança no core. O proibido é o oposto: mudar um comportamento de que alguém já depende, ou remover o que existe. A imutabilidade do tier 0 é a da promessa, não a do binário: o source conserta, a promessa não recua.
Tier 1, as bibliotecas oficiais. Um degrau acima: o que sofreu revisão nas últimas ~três décadas porque é especificação, não padrão. HTTP teve três versões nesse intervalo; o TLS gira de versão; um motor de regex troca de algoritmo. Não são padrões eternos como os floats; são contratos que evoluem, então patcham mais rápido que o core e têm cadência própria (semver, changelog, bugfix). É onde moram http, regex, containers. Vêm na toolchain como snapshot known-good (opt-in, peso zero até o use), mas atualizam independentes dela pelo registry first-party (a distribuição que isso exige é a fase de tooling, seção 22).
Tier 2, os frameworks. Mais volátil ainda: o que se reinventa na escala de uma década, frameworks web, frameworks de UI. É onde vive o web (sobre http) e o superset de UI (seção 20). A promessa de compatibilidade aqui é curta de propósito: quem está nesta camada aceita girar rápido em troca de poder de expressão.
Tier 3, terceiros. Os pacotes públicos, cuja volatilidade não é nossa pra controlar. E aqui o eixo vira de natureza: a fronteira tier 2 tier 3 não é “mais volátil ainda”, é uma virada de posse. Do tier 0 ao 2 somos nós que damos a promessa de compatibilidade; no tier 3 a promessa é de outro, o semver é deles. A correlação com volatilidade continua (terceiro tende a girar mais), mas o eixo real é quem é dono da promessa: um pacote de terceiro pode ser até mais estável que o nosso framework tier 2, e ainda assim é tier 3, porque a garantia não sai de nós.
| Tier | O que é | Gira em | Quem promete | Exemplos |
|---|---|---|---|---|
| 0, core | padrões/algoritmos do campo | décadas (add-only) | nós, intocável | a linguagem, floats IEEE, TCP/IP, List/Map/Set, UTF-8 |
| 1, libs oficiais | especificações que revisam | anos (cadência própria) | nós, versionado | http, regex, containers, TLS, IANA |
| 2, frameworks | o que se reinventa | rápido | nós, promessa curta | web, superset de UI |
| 3, terceiros | pacotes públicos | fora do nosso controle | eles, semver deles | qualquer dep do registry |
Duas afinações que o eixo-tempo esconde. A primeira separa coisas igualmente antigas em tiers diferentes: conjunto fechado vs roster aberto. collections (List/Map/Set) e containers (B-tree, ART, deque, ring buffer…) estão ambos cheios de algoritmos congelados há décadas; um B-tree é tão antigo quanto uma lista. O que os separa não é a idade do algoritmo, é que List/Map/Set é um conjunto completo e fechado (nunca haverá uma “quarta coleção fundamental”), enquanto containers é um roster que cresce (ARTs hoje, outra estrutura amanhã). Logo containers é um tier 1 empacotando conteúdo tier 0: o roster muda (tier 1), cada algoritmo dentro é frozen (tier 0), e é precisamente isso que o faz lib oficial e não core.
A segunda afinação: o tier corta por dentro de um mesmo conceito, separando mecanismo de dado. A codificação UTF-8 é frozen (tier 0); as tabelas Unicode, que ganham caracteres todo ano, são tier 1. O relógio UTC/offset é tier 0; os nomes de fuso IANA, que mudam com a política dos países, são tier 1. Os sockets e o IP de net são tier 0; as versões de TLS por cima, tier 1. O mesmo conceito se parte: a parte que é mecanismo fica no chão, a parte que é dado mutável sobe um degrau. Não é acaso que esse corte preveja decisões já tomadas por instinto, o núcleo de time com UTC/offset e o IANA como opt-in/heavy: quando o modelo prevê escolhas que você já fez, é sinal de que é real, não imposto.
E a estratificação dá de graça uma regra de dependência: os tiers só dependem pra baixo. http (1) desce a net/io/crypto (0); web (2) sobe sobre http (1); a UI (2) sobre o web; nunca o contrário. O core jamais importa uma lib oficial; uma lib oficial jamais enxerga um framework; e nada do que é nosso (0 a 2) depende de um pacote de terceiro (3). Isso não é arrumação de pastas: é a mesma disciplina de fronteiras de “Sistemas e fronteiras”, agora no eixo do tempo. Assim como um sistema não vaza cor para outro, uma camada não vaza dependência para cima. É o que mantém o core atualizável sem sacudir o que está acima, e o que deixa uma camada nova entrar amanhã sem reescrever as de baixo.