Sintaxe e convenções
Cinco princípios
Seção intitulada “Cinco princípios”A sintaxe inteira se justifica por cinco regras transversais, e cada decisão de grafia cai de uma delas, não de gosto:
- O default é o não-marcado. O caso comum e seguro nunca leva keyword; só se marca capacidade ou perigo extra. Vale para mutabilidade (
let/:=pelado vsvar/mut/*T/@transfer), visibilidade (pubpúblico,privprivado-ao-arquivo, privado-ao-módulo pelado) e compilação (comptimemarca; runtime é pelado). - Conceito = palavra, operação = símbolo. Se estrutura o programa, é keyword (
fn,match,loop); se é uma ação num ponto, é símbolo (<-,.,:=). - Modificadores ortogonais compõem. Um prefixo aplicado a várias coisas, em vez de uma keyword por combinação:
pub fn,comptime fn,@generator fn,comptime var. - Mesma ideia, mesma cara. Construtos conceitualmente iguais se parecem, e por isso
matchabsorve switch, select e o branch sobre erro de umResult(seção 7),loopabsorve for, while e infinito, e overloading é sómatchsobre tipo (seção 13). - Nome honesto: diz o que devolve e o que pode falhar. O nome entrega o resultado e a consequência de borda, sem decoder ring: nada de nome inócuo escondendo um panic (o anti-padrão
unwrap, rebatizadoor_panic, que mostra a consequência), nem de termo emprestado que não existe na linguagem (is_somevirouis_present, pois não hásome). É o “sem fluxo de controle escondido” aplicado a nome:or_panic/or(default)em vez deunwrap/unwrap_or;get -> Optional(“talvez”) distinto deat -> T(“eu garanto”);equalsem vez doeqlabreviado. Vocabulário universal e honesto (push/read/map) ou de domínio preciso (seal/opendo AEAD, que carrega o “autenticado” queencryptperderia) fica; o enganoso, não.
Declaração
Seção intitulada “Declaração”let/var/const, com o atalho :=; o imutável é o default (ver seção 6):
| Forma | Significado |
|---|---|
x := v |
imutável, runtime (atalho, caso comum) |
let x := v |
imutável, runtime (explícito) |
var x := v |
mutável, runtime |
const x := v |
constante de compilação (= comptime let) |
Tipo explícito ao estilo Odin (x: T = v); := é : = com o tipo omitido. A regra: := (ou : T =) declara; = pelado só atribui a um var, pois o : marca declaração. Descartar um retorno é explícito: _ := expr (o _ é a variável-ausente, seção 28); largar um valor no chão sem _ := é erro, porque descartar é uma intenção, e intenção se escreve. comptime é o modificador ortogonal de “avalie em compilação” (comptime fn/let/var/loop/match/{}; ver seção 10).
Binding múltiplo e tupla. Vários valores de uma vez formam uma tupla (a, b) (seção 7): coords := (10, 20) liga a tupla a um nome. Para ligar a vários nomes (atribuição paralela, ou desestruturar um retorno-tupla) as duas grafias valem e casam por posição: a, b := f() (nomes soltos) ou (a, b) := f() (com os parênteses da tupla). Trocar dois valores é a, b = b, a.
Loop unificado
Seção intitulada “Loop unificado”for, while e loop infinito diferem só no cabeçalho, não no corpo, então há uma keyword, loop, e o cabeçalho decide a forma:
loop { } // sem cabeçalho → infinitoloop cond { } // expressão booleana → whileloop x in xs { } // IDENT in iterável → for-each (coleção, generator, range)loop i in 0..8 { } // range exclusivo → contado (..= é inclusivo)loop i := 0; i < 8; i = i+1 { } // C-style; os ';' delimitam as 3 partes (parênteses opcionais)break/continue funcionam igual em todas. No C-style os parênteses são opcionais (estilo Go): numa linha, os ; explícitos já delimitam as três cláusulas (a inserção automática de ; só age em quebra de linha, e aqui não há nenhuma), e o { fecha o cabeçalho. Os parênteses só são necessários se você quebrar o cabeçalho em várias linhas (ou para isolar um literal composto no cabeçalho, como em Go).
match unificado
Seção intitulada “match unificado”Um construto para “dado um valor, escolher um galho”, que absorve o que outras linguagens dividem em switch/match/select, mais o branch sobre o erro de um Result (seção 7). O compilador distingue três formas pelo token logo após match: um sujeito casa padrões (o “switch”); { abre operações de channel (o “select”); |e| pendurado num op ramifica o erro daquela operação:
// com sujeito → casa padrões (o "switch"/"match clássico")match resultado { Ok(v) => usa(v) Err(e) => trata(e)}match código { 200 | 201 | 204 => ok() // or-pattern com '|' (cobre o caso do fallthrough) 404 | 410 => sumiu() _ => outro() // default é sempre '_' explícito}
// sem sujeito (só '{') → casa operações de channel prontas (o "select"; ver seção 4)match { v := -> chan_a => handle(v) _ := -> chan_b => sinal() // '_ :=' descarta o valor recebido} timeout(5s) { // timeout é trailer do select; timeout(0) = não-bloqueante expirou()}
// trailer '|e|' num op → ramifica o erro de uma operação falível (o Ok vira a variável; seção 7)v := do_something(x) match |e| { E1 => { log(e); return Err(e) } // value-binding: o arm escapa (loga, propaga) E2 => runtime.panic(e) // ou aborta}O select (match {}) aceita um timeout(...) como trailer: o bloco roda se nenhum channel responder no prazo; sem ele, o select espera indefinidamente, e timeout(0) é o poll não-bloqueante. A forma op match |e| é a que pendura o match numa operação: o sujeito (o erro) vem por posição e o |e| o nomeia. Só vale com erro união; variante única é erro de compilação (use catch; seção 7).
Exaustividade: em sum type ou enum (domínio fechado) o compilador exige todos os galhos e proíbe o _. Um _ num match exaustivo é erro de compilação, porque anularia a rede de segurança: adicionar uma variante cairia em silêncio no _ em vez de quebrar a build até você tratá-la (a mesma régua do “capacidade declarada e não-exercida é erro”, seção 6). Em domínio aberto (int, string) o _ é obrigatório, sendo o default-case (não há keyword default; o _ faz esse papel), exigido porque o domínio do valor não tem escopo fechado. Não há fallthrough; o or-pattern | cobre “vários valores, mesmo galho”. Os arms se separam por quebra de linha (não por vírgula), um por linha; numa linha só, separam por ; (o terminador de statement, como em unsafe { a; b }), nunca por vírgula. A vírgula fica para grupos-de-valor (argumentos e tuplas em (), conjuntos type-level, e literais de array/slice []int{1, 2, 3}); os corpos de membros distintos (campos e variantes de decl, literais de struct Vec2{1; 2}, arms de match) separam por ;. O separador segue o conteúdo, não o bracket: um literal de struct e um literal de array usam ambos { }, mas os campos da struct são membros distintos (;) enquanto os elementos do array são um grupo-de-valor (,).
return, break, panic: nada implícito
Seção intitulada “return, break, panic: nada implícito”A linguagem não tem retorno implícito. Um bloco não vale a sua última expressão; o => de um arm não entrega o lado direito, pois ele só separa padrão e corpo, e o corpo (como os arms do select de canal) roda lógica. Para produzir um valor ou sair, sempre explícito: return, break, panic.
return X tem um único sentido, decidido pela posição do construto:
- expressão (o construto produz um valor):
return Xentrega X ao lugar que recebe a expressão.v := match f() { Ok(x) => return x }dáxav;x := loop { ... return v }dávax. Isso vale promatche proloop;ifé statement, então um valor escolhido por condição é o ternário (v := c ? a : b). - statement (roda solto):
return Xé early-return, ou seja, sai da função.if c { return err }propaga;f() match |e| { Timeout => return e }sai.
O arm ou braço herda a posição do construto; não há regra por arm. panic aborta; break sai da loop (veja abaixo); ambos divergem sempre.
loop como valor. Um loop pode estar em posição de valor (x := loop { ... return v }), entregando por um return explícito do mesmo jeito que um match em posição de valor. É o que o if deliberadamente não tem. Um loop em posição de valor precisa entregar em todo caminho de saída (como o corpo de uma função): um loop { ... return v } infinito precisa de um return alcançável; um loop condicional/for precisa return em todo caminho do corpo, ou o checker aponta o caminho que pode terminar sem valor. (Um corpo que sempre retorna ainda precisa de ao menos uma iteração; o caso coleção-vazia / condição-imediatamente-falsa é pego em runtime.) Um loop que só itera por efeito é um statement comum, inalterado. Isso convive com o caminho funcional (xs.reduce(...), o .collect() de um generator): esses transformam uma sequência que já existe, enquanto um loop em posição de valor computa um valor do zero.
break e continue: sem valor, label opcional. break sai de um loop e continue pula pra próxima iteração; nenhum carrega valor (um loop entrega seu valor por return, então um break que sairia de um loop em posição de valor é erro). Um break/continue pelado mira o loop mais interno; uma label mira um loop nomeado, escrita estilo Go antes do loop:
outer: loop x in xs { loop y in ys { if found(x, y) { break outer } // sai dos dois loops if skip(y) { continue outer } // próximo x }}Uma label é só o nome de um loop (outer:); ela se distingue de um binding tipado (x: T) pela keyword loop que segue os dois-pontos.
return distribuído (açúcar). Um match em posição de valor exigiria return em cada braço, o que é verboso. O atalho: return match ... distribui o return pros braços. return match s { Idle => Running } equivale a match s { Idle => return Running }. As duas formas convivem; escolha pela leitura. (Em value-binding não há return externo pra distribuir, então os braços levam o return: v := match f() { Ok(x) => return x }.) O => continua sem entregar por conta própria: a entrega mora sempre num return explícito, no braço ou no que distribui. O if não entra nisso: if é statement, nunca valor, então pra escolher entre dois valores por condição você usa o ternário abaixo, não um if.
Ternário
Seção intitulada “Ternário”Pra escolher entre dois valores por uma condição, o ternário cond ? a : b, uma expressão:
icon := liked ? "♥" : "♡"max := a > b ? a : bÉ açúcar pra a escolha binária que o match faria verboso (match cond { true => return a; false => return b }). Vale só pra duas alternativas; três ou mais é match (ternário aninhado vira sopa, e o match lê melhor).
Lambdas
Seção intitulada “Lambdas”Uma função anônima passada inline a outra função. Duas formas, ambas só em posição de argumento:
xs.filter(fn(l) => l.contains("ERROR")) // forma curta: uma expressão, '=>' entregaxs.map(fn(l) { parse(l); return enrich(l) }) // forma de corpo: várias linhas, 'return' explícitoNo lambda de uma expressão, o => entrega o lado direito, e isso não contradiz o => do match (que não entrega), porque os dois são coisas diferentes: um lambda é, por definição, um produtor de valor (existe pra computar um resultado), enquanto um arm de match é um galho (faz algo naquele caso). fn(x) => e é só açúcar de fn(x) { return e }; o => entrega porque o corpo do lambda é o retorno. No fundo (filosofia, seção 1), esse => é o marcador explícito de saída (o análogo do return, da família do ?: do ternário), então a forma curta não viola o não-retorno-implícito: você não adivinha que e é a saída por posição (“é a última expressão”), você lê no =>. O preço é o glifo => ter dois papéis (saída no lambda, separador no arm), aceito porque os dois nunca se cruzam (no lambda o => é seguido de expressão; no arm, de corpo) e “maps to” é a intuição comum aos dois.
Lambda é só encurtador de anônima, não substitui declaração. As duas formas valem exclusivamente como argumento direto de uma chamada. Ligar a uma variável (f := fn(x) => …), retornar, guardar em struct, ou usar no topo do arquivo é erro de compilação; pra função nomeada, reutilizável ou de topo, é fn nome(...) { ... }. A cerca é proposital: corta na origem o const f = x => … espalhado (as arrow functions de JS viram nome de variável em todo canto); aqui, anônima não se liga e não persiste, e quem quer reusar dá nome. Essa cerca é a regra do escopo transient (seção 6): lambda é uma expressão transient, que existe só cruzando a fronteira de uma chamada, nunca solta.
A cerca é contra a função anônima, não contra a nomeada, e não é uma cerca contra dar nome a um escopo. Daí saem duas consequências, que puxam pra lados opostos de propósito.
Função nomeada também não liga a variável — g: fn(int) -> int = double é erro de compilação. Não porque vazaria escopo (função nomeada não fecha sobre nada), mas porque isso é renomear: double já tem nome, e o jeito de chamar de outra coisa é trocar o nome dela. Um binding que só apelida uma função existente não compra nada e custa um segundo nome pra uma coisa só. Quer retornar uma função, ou reusar uma? Retorna, ou chama, pelo nome. (Isso resolve a aparente abertura do “ou um binding de tipo função” da seção 6: o que segura uma função é um parâmetro de tipo função — o callback que a recebe — não um binding local.)
Uma função pode ser declarada dentro de outra. fn a() { fn b() { ... } } é válido: b é visível só dentro do corpo de a (sombreando um b de topo de mesmo nome), pode ser chamada antes da declaração, irmãs podem chamar umas às outras, e ela pode recursar. Não obriga todo auxiliar a ser global só porque é usado uma vez — nem toda função merece escopo de módulo. Mas aninhar é escopo, não captura: b não enxerga os locais de a; tudo que ela precisa chega por parâmetro. Fechar sobre um escopo continua exclusivo da lambda, que paga por esse poder sendo transient. Assim a função aninhada é a resposta pra “preciso de um auxiliar aqui”, e a lambda é a resposta pra “preciso desta receita, agora, sobre estes valores” — e nenhuma das duas vira a arrow function solta do JS.
Cleanup determinístico em qualquer função ou bloco; roda na saída do escopo (incluindo return antecipado), múltiplos defers em LIFO. É o mecanismo de liberação para @mm(none)/manual:
fn read_config() -> Result[Config, error] { f := open("cfg") catch |e| { return e } defer f.close() // roda ao sair, em qualquer caminho ...}Os argumentos são congelados no defer; só a chamada é adiada. defer f.close() fecha o f daquele ponto: o receptor e cada argumento são avaliados quando o defer é alcançado, e a chamada roda depois com esses valores. É esse o sentido do construto — adiar aquele call site, com as entradas naquele estado. Ler os argumentos depois redirecionaria a limpeza em silêncio: reatribua f após o defer e você fecharia o arquivo errado, que é exatamente o bug que o defer existe pra evitar. Uma limpeza que precisa enxergar um valor posterior não é uma chamada adiada: mova a chamada pro fim do escopo e passe o que você quer. Congelar um agregado congela o valor, como qualquer passagem por valor (§6), então um p.x = 9 posterior não é visto pela limpeza.
var n := 0defer show(n) // congelado: mostra 0n = 9 // ... não 9Durações e timeout
Seção intitulada “Durações e timeout”timeout é keyword: o deadline obrigatório do <-> (seção 4, onde vira error.Timeout no Result) e o trailer do match sem sujeito (o bloco que roda se nenhum channel responder no prazo). O valor é um Duration (tipo builtin, u64 nanossegundos; duração nunca é negativa), escrito com literais de unidade de lexer: 5s, 300ms, 10ns, 6m, 7h, 18d, notação literal (como 0xFF ou 1_000), convertida pelo lexer para Duration, sem importar nada. timeout(_) é o deadline infinito (explícito, sem prazo).
reply := chan <-> req timeout(5s) // deadline no <-> → error.Timeout no Resultmatch { ... } timeout(100ms) { ... } // deadline no select → trailer com bloco (seção 4)chan <-> req timeout(_) // <-> sem deadline (infinito, explícito)O pacote time (stdlib) traz aritmética rica e durações de precisão arbitrária (Duration[T]/ArbitraryDuration sobre qualquer inteiro, como picosegundos); o Duration builtin do timeout é o u64-ns, e quem precisa de mais importa time (opt-in puro).
Sigil @
Seção intitulada “Sigil @”Um sigil só, @, para tudo que fala com o compilador. A regra é se há configuração:
- Diretiva com argumento usa parênteses:
@mm(arena),@limit(16mb),@supervisor(strategy: rest_for_one),@promote(gc, deep)(postfixa no valor:result @promote(gc); ver seção 5). - Tag pura dispensa parênteses:
@transfer(postfixa no valor:chan <- result @transfer),@generator(prefixa emfn).
@transfer substitui a antiga notação #(...); @generator fn substitui a keyword generator. (Não existe @shared; ver seção 5.)
unsafe, assert e assume
Seção intitulada “unsafe, assert e assume”Os construtos que marcam e contêm o perigo de memória (semântica completa na seção 5). unsafe é um modificador ortogonal (Princípio 3), como pub/comptime/@generator; assert/assume são o trailer de contenção de uma operação unsafe, espelhando o catch na forma mas não na semântica:
unsafemarca a região ou operação onde as garantias estão suspensas, em quatro formas:unsafe expr(operação única),unsafe { }(grupo),unsafe fn(a função repassa perigo ao chamador),unsafe union(declaração marcada). Emfn,unsafeé a consequência forçada de haver operação unsafe não-abatida no corpo, não uma escolha de grafia; abater tudo comassert/assumeno corpo dá umfnnormal. Os modificadores de função ficam empub? comptime? unsafe? fn;@generatoré um decorador como os demais (acima da função ou inline), sem slot fixo.assert/assumesão os lidadores, sempre trailer de uma operação, nunca no header (carregam lógica, não um bit). Duas keywords, uma intenção cada:assert (cond)checa, e o runtime verifica e dá panic se falsa;assume "razão"confia, afirmação documentada sem check (e sem licença pra UB). Não abater é repassar (a função viraunsafe fn).@requires(cond)e a pós-condição (no tipo de retorno) são o contrato de fronteira da função: design by contract, diretivas@(zero keyword), distintas doassert/assumede operação. O compilador os checa em cada chamada ereturn: estático onde prova, panic onde não. Uma pré-condição checável e contratada torna a função segura de chamar (o chamador não re-declara; é a camada que constrói safe sobre unsafe). Múltiplos@requiresempilham; reuso de predicado é função booleana comum (sem construtocontract, que não passaria no teste do opt-in, seção 1).
b := unsafe raw_read(buf.ptr + i) assert (i < buf.len) // forte: checa, panic se falsay := unsafe x.bits assume "float→bits" // fraca: afirma, sem checkunsafe { p.next = q; q.prev = p } assert (p != q) // trailer de blocoMódulos
Seção intitulada “Módulos”use é a única keyword de import: traz o módulo (acesso qualificado), nomes específicos, ou uma chamada inline fora do topo:
use json // monta o módulo → json.parse(...)use json.{parse, decode} // levanta nomes → parse(...) diretouse json as j // aliasuse json.parse(data) // inline: chamada pontual, sem trazer nada pro escopo do arquivo(O sistema completo, com unidade, resolução de path, visibilidade, pacotes, comptime e inicialização, está na seção 15. Aqui fica só a keyword use.)
Tipos e literais
Seção intitulada “Tipos e literais”-
Inteiros estilo Zig, com largura de bits arbitrária de 1 a 65535:
iN/uNpara qualquerNnesse intervalo, em quei3,u7,u777ei65535são válidos. Não é lista fixa, é família paramétrica;i8/u8/i16/u16/i32/u32/i64/u64/i128/u128são só os casos comuns dela.isize/usizepara largura de ponteiro;int/uintcomo apelidos da largura default;byte=u8,bit=u1. Largura não-múltipla-de-8 se comporta com a largura declarada (a aritmética de umu3é checada contra o intervalo de 3 bits) mas ocupa o próximo tamanho endereçável (umu3solto ocupa 1 byte; um elemento de[]u3/[*]u3também ocupa 1 byte cada, endereçável e indexável, ao custo de 5 bits desperdiçados; ocupa os 3 bits reais só dentro de umpacked struct). Não se toma*Tde um campo sub-byte (nem solto, nem depacked), e o motivo é a memória ser endereçada a byte: um ponteiro normal guarda um endereço de byte, e não há como ele nomear “os 3 bits no offset 3 dentro deste byte”. Permitir (como o Zig faz) exigiria um tipo de ponteiro-de-bit separado, que carrega o offset e não interopera com ponteiro normal, ou seja, duas espécies de ponteiro no sistema de tipos. Escolhemos uma espécie só (mais simples); se a necessidade aparecer, esse ponteiro-de-bit pode ser adicionado então. Na fronteira C (@repr(c)/@extern, seção 23), uma largura sem ABI C definida (u3, já que C não temu4) é erro de compilação, sem widening silencioso; pra cruzar você escolhe explicitamente um tipo C-compatível (u8).Overflow trapa por padrão.
+,-,*dão panic em runtime quando o resultado verdadeiro sai do intervalo da largura (aritmética checada, como Zig e Rust em debug), o que é o que torna a largura um invariante real em vez de um truncamento silencioso. Uma expressão constante que estoura é erro de compilação: o checker dobrax: u8 = 200 + 100e rejeita, não espera o runtime. Quando você quer aritmética modular pede no operador:+%,-%,*%dão wrap em complemento de dois, então um wrap é sempre visível no código em vez de implícito. As conversões de largura que perdem informação (300 as u8,3.9 as i32) passam pelo castasexplícito pela mesma razão. O shift bit a bit<<é a exceção, e de propósito: um shift é uma operação de bits sobre a largura, não uma contagem aritmética, então os bits empurrados além do topo são mascarados para a largura ((19: u8) << 4é304 mod 256 = 48), o significado C/hardware de um shift. Não trapa e não existe<<%, porque um shift para fora da largura é a operação funcionando como definida, não um overflow de magnitude. (Use*/*%quando quiser “multiplicar por potência de dois” com as semânticas de trap / wrap de magnitude.) -
byteebit: convenção, não regra. Uma ressalva honesta, do mesmo espírito da unidade de string (seção 16) e do byte da FFI-C (seção 23): por definição,bytenão tem tamanho fixo. Ao longo da história foi 3, 4, 6, 7 e 9 bits, sempre “um grupo de bits”, nunca um tamanho prescrito; o octeto (8 bits) é a convenção que venceu, não uma lei da computação. A linguagem adota essa convenção (byte=u8) porque todo alvo que ela compila é octeto e porque a alternativa, umbyte[N]genérico, destruiria a ergonomia dos literais de tamanho sem ganhar poder nenhum. Poder nenhum porque “um grupo de N bits” já é ouN: a precisão arbitrária de bits é a famíliaiN/uN(acima), e quem quer 3 bits escreveu3, nãobyte[3]. Entãobyteé só o nome do octeto (o átomo de armazenamento, o elemento de[]byte, a unidade de IO) ebité só o nome amigável dou1; a largura mora no tipo (uN), e os dois são apelidos de casos seus. (Num alvo não-octeto hipotético, casar o octeto com a unidade nativa seria trabalho daMemorySourcedaquele alvo (seção 5), não do tipobyte, que permanece a convenção universal.) -
Floats nas larguras IEEE:
f16/f32/f64/f128/f256(maisbf16para ML);floatcomo apelido default. A família fixa para onde o IEEE 754 para de nomear formato de uso real: o padrão definebinary16/32/64/128/256(e, em princípio, qualquerbinary{32k}acima de 128, que ninguém usa), então a linguagem traz esses cinco e nada no meio. Largura arbitrária não é um float fixo do jeito queuNé um inteiro fixo, porque não há formato canônico nem hardware pra ela: um float mais largo tem que escolher bits de expoente versus mantissa, escolha que o lado inteiro nunca enfrenta (um grupo de N bits já éuN). Quando você de fato precisa de mais de 256 bits de float, recorre aoBigFloatabaixo, que é uma ferramenta diferente com custo diferente. -
Um
floaté sempre um número real finito:NaNé erro, e overflow trapa como o do inteiro. É o lado float da mesma honestidade que faz+trapar no overflow de inteiro.NaNé literalmente não-é-número: uma operação que o produziria (0.0/0.0,inf - inf,0 * inf, raiz de negativo) é erro de runtime, nunca um valor que propaga em silêncio. (Propagação silenciosa de NaN é exatamente o unknown-escondido que a linguagem gasta esforço pra banir, seções 1 e 14.) Estourar a largura também não dáinf: trapa, igual overflow dei8, então umfloatque você segura é garantidamente finito e comparável. O ganho é uma ordem limpa: semNaNe seminf, todo float é totalmente ordenado, então<devolve umboolsimples e não há necessidade das duas funções de comparação (um<parcial e umislesstotal) a que as linguagens que mantêm oNaNdo IEEE são obrigadas. Infinito real e direcionado (um limite, trabalho projetivo) é umExtendedFloatopt-in separado e deliberado (math, o modelo Mathematica:+inf/-infsão valores direcionados definidos, enquanto um0/0genuinamente indefinido continua erro, nunca uminf). Você pedeExtendedFloatquando a ciência precisa do ponto no infinito; ofloatdo dia a dia continua finito por padrão, porqueinf-por-default é justamente o valor que começa a fazer as vezes de “unknown”. -
Comparação: ordem total pro finito, ordem parcial só pra bola rigorosa. Como um
floaté sempre finito (acima), eint/Decimal/BigInt/BigFloatsão exatos, todos carregam uma ordem total:<><=>===!=devolvemboolsimples, e oOrdering { Less; Equal; Greater }compartilhado (um enum builtin, comoOptionaleResult, já que o resultado de uma operação tão fundamental não deve ficar atrás de um import) é o resultado de um.cmp()de três vias, o tipo que um tipo de usuário implementa pra virar ordenável (o comportamentoComparable;<=/>=/!=são derivados dele, não primitivos separados). A única exceção é oBigFloatArb(a bola com limite de erro): duas bolas cujos intervalos se sobrepõem não têm ordem decidível (o verdadeiro pode cair pra qualquer lado). Seria mentira o<devolverboolali, então a bola não tem operadores de comparação; tem um método.cmp(b) -> ArbOrdering, ondeArbOrdering { Less; Equal; Greater; Indeterminate }adiciona o quarto caso, e omatchexaustivo te obriga a encarar oIndeterminate(aumenta a precisão e re-tenta, ou decide você). É a separação padrão ordem-total-versus-ordem-parcial (oOrdvsPartialOrddo Rust), e fica contida:Indeterminateé um variant de enum definido local ao resultado da bola, exatamente comoOptional.noneé um variant definido, não um valor “unknown” vazando pro sistema de tipos. Um booleano de três valores é recusado de propósito: umboolque pode ser “talvez” é um unknown contrabandeado pro único tipo cujo trabalho inteiro é certeza, e envenenaria todo&&/||/ifcom lógica de três valores (tainted coloring, seção 1). A incerteza da aritmética de intervalo mora em um variant de um método, e em nenhum outro lugar. -
Precisão arbitrária mora em
math(stdlib), não no escopo global, mas lê como aritmética comum. É a resolução deliberada de duas forças: o não-envenenamento mantém esses nomes fora do escopo de todo programa (você nunca paga porBigIntem código que não o nomeia, o teste do opt-in), enquanto o privilégio de operador (Convenções, abaixo) os abençoa como numéricos-core pra usarem+ - * /como qualquer número, nuncaa.add(b). Um tipo de biblioteca nunca poderia ter os operadores (sem overload, seção 13); é justamente por isso que precisão arbitrária é core-blessed em vez de biblioteca pura. Cinco tipos, cada um uma ferramenta distinta:BigInt: inteiro ilimitado, sem parâmetro de largura. Não trapa, cresce (sinal mais um slice de limbs), que é o oposto conceitual limpo doiNque trapa. Pra contagem exata, criptografia, teoria dos números.BigFloat[M]: número de ponto flutuante de precisão arbitrária comMbits de mantissa de precisão de trabalho, corretamente arredondado a cada operação (o modelo MPFR). Precisão é relativa:Mbits significativos onde quer que o valor esteja na reta. É o cavalo-de-batalha da computação científica (magnitudes muito grandes ou muito pequenas, funções especiais, constantes).BigFloatArb[M]: o mesmo float deMbits carregando um limite de erro rigoroso (o modelo Arb/FLINT: um ponto médio e um raio), então depois de uma cadeia de operações você sabe quais dígitos sobreviveram. Custa mais (cada operação rastreia o limite) e você o pede quando precisa de certeza em vez de um número que só parece preciso (cancelamento catastrófico que você precisa detectar, resultados que sustentam uma prova).DecimaleDecimal[P, S]: números exatos em base-10, pra dinheiro e contabilidade. A base é o ponto inteiro:0.10é exato em base-10 e dízima em base-2, e é por isso que moeda nunca anda sobre float binário.Decimalé de precisão arbitrária (umBigDecimal, com o arredondamento tornado explícito numa divisão);Decimal[P, S]é a forma limitada (Pdígitos significativos totais,Scasas decimais, o formato doDECIMAL(p, s)do SQL) pra quando você quer o tamanho fixado no tipo.
Por que não aritmética de significância. Existe uma terceira escola de float (a do Mathematica: rastrear quantos dígitos seguem confiáveis e descartar o resto, adaptativamente). A linguagem não a traz, de propósito. Ela torna a precisão dinâmica e implícita, escondendo justamente a decisão que as outras duas tornam explícita:
BigFloat[M]faz você escolherM,BigFloatArbfaz você pagar um limite visível; a significância decide por você e muda de ideia a cada operação. É o tipo de mágica prestativa que todo o design rejeita (o custo tem que ser visível, seção 1). Quem precisa de precisão adaptativa a compõe a partir deBigFloatArb(re-roda mais largo até o limite ficar apertado o bastante), explicitamente, em vez de recebê-la como default silencioso.Literais resolvem por contexto, como um caractere (seção 16). Um literal numérico é comptime sem tipo fixo, exatamente como
5virau8/i64/inte'w'virabyte/codepoint/grapheme. Ele toma o tipo declarado do binding:x: BigInt = 123456789012345678901234567890,x: Decimal = 9.99. Quando o contexto não desambigua, você tem que anotar: umx := <literal acima de i64>solto é erro pedindo um tipo, não promoção silenciosa praBigInt, a mesma disciplina que faz'é'.byteser escolha explícita em vez de chute. -
Optional[T]para ausência;[N]Tarray dimensionado,[]Tslice,[*]Tponteiro-de-muitos (buffer cru que se possui; seção 6). -
noreturn, o tipo da divergência: o de uma função que nunca devolve o controle ao chamador. É distinto da ausência de valor de retorno:fn f()retorna (o controle volta, só não traz valor; efn()é o tipo dessa função, sem precisar de umunit), enquantofn panic(msg: string) -> noreturnnão retorna (não existe “a linha depois”). Divergem:loop {}infinito,panic,exit. Não é construto especial de builtin, pois código de usuário também declara-> noreturn(um event-loop, umabort), e a regra do type-checker é uniforme: um valornoreturnencaixa em qualquer contexto de tipo, porque o caminho que o produziria nunca chega lá. É isso que fazx := parse(s) catch |e| { panic(e) }tipar comx: int: o braço de erro chamapanic(-> noreturn), diverge, e o caminho de erro nunca alcança a atribuição, entãoxé ointdoOk. (Em teoria de tipos é o bottom; o nome fala do que importa na prática, o retorno de controle, não “nunca”.) Um canto dofn(): como o retorno é ausente (não um valor, nãonoreturn), um genérico que abstrai sobre o tipo de retorno,g[R](f: fn() -> R), não casa comfn()(não háRa que ligar). É raríssimo e contornável (envolva numfn() -> Empty); fica registrado porque a generalidade “genérico sobre o retorno” não cobre a função-sem-retorno. -
Literais de tamanho, no molde dos de duração (acima): notação de lexer convertida para
usizeem bytes, sem importar nada. O átomo é o byte; ordens de grandeza acima dele vêm em base-10 e base-2, distintas pelo prefixo:512b(bytes),4kb/4kib(10³ vs 2¹⁰),16mb/16mib,2gb/2gib,8tb/8tib. Obé byte; oi(base-2) só aparece com prefixo (kib, nunca512ib). Não há literal de bit: bit é largura-de-tipo (uN), não quantidade-de-memória; memória é endereçada a byte, então tamanho conta bytes, e número pelado num contexto de tamanho (@limit(512)) conta bytes, o átomo. Para valor de runtime, os helpersmem.kib(n)/mem.mib(n)(stdlib), simétricos comtime.millis(n): literal para o estático, helper para o dinâmico. -
Literal de struct posicional ou nomeado:
Vec2{1.0; 2.0}ouVec2{x: 1.0; y: 2.0}.
Ausência, valor-default e null são três coisas, e a linguagem só tem a primeira. Optional[T] é ausência tipada (none/some), a única forma de dizer “pode não estar aqui”. Não existe null (sem ponteiro, referência ou binding nulo, invariante verificado por grep na stdlib). E não há zero-value implícito estilo Go: todo binding nasce com um RHS explícito (seção 10, “não há undefined”), então 0, "" e List.new() só surgem quando você os escreve, e aí são valores presentes e reais, não “faltando”. Um int que vale 0 ≠ um Optional[int] que é none ≠ um campo que não chegou; um "" ≠ ausência. Default é sempre explícito: o literal que você digita, ou um argumento (opt.or(d), map.get_or_insert(k, default)), não um preenchimento automático do tipo. (Só mem.zero([]byte) zera bytes crus, no nível de memória, e não é um zero-value de tipo.) Num formato de fio, o token null (o null do JSON, por exemplo) é um valor do modelo do formato (uma variante de um enum Value), e mapeia pra Optional.none só na fronteira; não é um null da linguagem (ver a stdlib encoding).
Convenções
Seção intitulada “Convenções”snake_casepara valores e funções;PascalCasepara tipos. É convenção, não regra de escopo: visibilidade épub, nunca capitalização (ao contrário do Go).- Ponto-e-vírgula opcional, auto-inserido no fim de statement (estilo Go); como consequência, chaves
{}estilo K&R ({na mesma linha). - Pipeline com
.via UFCS (x.f()equivale af(x)):xs.filter(p).map(g).collect(). Não há|>. - Concat de string com
+; sem operador dedicado. - Operadores aritméticos (
+-*/) são privilégio dos tipos numéricos do core: a famíliaiN/uN/float, o core-blessedDuration(builtin), e a família de precisão arbitrária emmath(BigInt,BigFloat[M],BigFloatArb[M],Decimal/Decimal[P, S]). Tipo definido por usuário não ganha overload de operador: usa método nomeado (a.add(b)), coerente com a seção 13 (semoverload) e com “operação = símbolo só para o que o core define”. A régua resolve a aparente tensão (BigInteDurationtêm+, coleções não): o símbolo aritmético é do numérico-core; o resto nomeia. Ser core-blessed é o que deixa os tipos demathvestirem operadores enquanto ficam fora do escopo global (seção 28): a benção é sobre o símbolo, não sobre onde o nome mora. - Sem operador
?de propagação de erro (ver seção 7): a propagação é explícita viacatch.