1 pontos por GN⁺ 2025-11-08 | 1 comentários | Compartilhar no WhatsApp
  • O compilador Zig, que oferece compilação de código C e compilação cruzada nativamente, é a linguagem mais surpreendente que o autor encontrou em 45 anos de experiência
  • Com recursos únicos como execução em tempo de compilação, variáveis com tamanho arbitrário em bits e ambiente de blocos de teste, ele vai muito além de um simples substituto de C/C++ e oferece uma forma totalmente nova de programar
  • Com sintaxe concisa e clara, como declaração de variáveis por inferência de tipos, structs anônimas e label break, é possível aprender rapidamente
  • Com testes independentes de módulos por meio de blocos de teste e a função embutida @breakpoint, oferece suporte à depuração de código otimizado
  • Com suporte à programação de baixo nível usando bit fields e operações de bits, alcança ao mesmo tempo eficiência e robustez, integrando a uma linguagem compilada as vantagens de linguagens interpretadas

Prefácio

  • Em 45 anos de carreira, o autor nunca encontrou uma linguagem tão surpreendente quanto Zig
    • Zig não é apenas uma nova linguagem, mas uma ferramenta que muda fundamentalmente a forma de programar
  • Vê-la apenas como um substituto de C ou C++ é uma grande subestimação
  • O objetivo deste texto é apresentar recursos simples, mas atraentes de Zig e ajudar programadores a começar rapidamente
  • Existem muitos outros recursos que influenciam a adoção de Zig na indústria

O compilador Zig

  • Oferece compilação de código C e compilação cruzada por padrão, sem configuração adicional, o que tem grande impacto na indústria
  • A instalação é feita baixando o compilador para o processador/SO correspondente na página de downloads do Ziglang, descompactando-o e copiando-o para o diretório desejado
    • No Windows 10, recomenda-se copiar o arquivo zip x86_64 para Program Files e renomear o diretório raiz para zig-windows-x86_64, evitando a necessidade de alterar a variável de ambiente Path a cada atualização de versão
    • Depois de adicionar o caminho do diretório raiz à variável de ambiente Path, o compilador pode ser usado no modo CLI
  • Para compilar o programa "Hello World!", recomenda-se consultar a seção "Getting Started" do site oficial

Conceitos e comandos principais

Declaração de variáveis

  • A declaração de variável é composta por uma primeira parte com acessibilidade (pub ou omitido), var/const e nome da variável, uma segunda parte com a declaração de tipo e uma terceira parte com a inicialização
    • Apenas a primeira e a terceira partes são obrigatórias, e o tipo pode ser inferido a partir do valor de inicialização
    • Exemplo: var sum : usize = 0;
  • Variáveis declaradas sem pub só podem ser acessadas dentro do módulo (semelhante a variáveis static em C)
  • Não é recomendado declarar variáveis pub, e é recomendado minimizar funções pub para reduzir acoplamento e aumentar coesão

Structs, structs anônimas e blocos de teste

  • Literais de struct anônima envolvidos por .{ e } são usados para inicializar elementos de outra struct ou criar uma nova struct com elementos inicializados
  • .{ } é um literal de struct anônima vazia
  • A forma struct { } é uma declaração de struct
  • Blocos de teste permitem compilar e executar testes sem um executável

Bit fields

  • Bit fields são declarados como campos de tipos com tamanho específico dentro de uma packed struct
  • Ponteiros podem apontar para um bit field específico

Laço for

  • A sintaxe de Zig é mais clara que a de C, mas usa intervalo aberto [0..9) em vez de [0..8]
  • A declaração de tipo, inicialização, teste e incremento da variável de laço i são tratados automaticamente

Arrays

  • [_] define um array com tamanho desconhecido, seguido pelo tipo dos elementos e pela inicialização
    • Exemplo: var grid = [_]u8{0} ** 81; inicializa 81 elementos u8 com 0
    • O tamanho do array é inferido a partir do argumento de repetição da inicialização
  • Em ambiente de teste, é possível percorrer os elementos do array e somá-los
  • A variável declarada entre | no laço for é automaticamente assumida como do mesmo tipo dos elementos do array
  • usize é o inteiro sem sinal natural da plataforma (u64 em 64 bits, u32 em 32 bits)

Ponteiros multi-item

  • Para que um ponteiro para array use aritmética de ponteiros, ele precisa ser explicitamente declarado como ponteiro multi-item, como [*]const i32
  • Mesmo que o array seja const, o ponteiro pode ser declarado como var

Desreferenciação de ponteiros

  • Um ponteiro ao qual foi atribuído o endereço de uma posição individual de array não pode ser atualizado com aritmética de ponteiros
  • A desreferenciação de ponteiro usa ptr.*

Label break

  • É possível realizar diversas tarefas em tempo de compilação, como inicializar arrays
  • No label break, acrescenta-se : após o nome do bloco, e break retorna um valor a partir do bloco
    • Exemplo: break :init m;
  • 0.. é um intervalo infinito começando em 0
  • No laço for, as variáveis são inicializadas e incrementadas automaticamente, e o laço termina após processar a última posição do array
  • Um array pode não ser inicializado explicitamente com undefined

Funções em Zig

  • Funções são declaradas com fn e, por padrão, são static (usadas apenas dentro do arquivo)
    • Quando declaradas como pub fn, podem ser importadas por outros arquivos
  • Funções podem ser "inlined"
  • Ponteiros para função vêm com const na frente e o protótipo da função em seguida

Programação orientada a objetos em Zig

  • Structs podem ter funções
  • No exemplo da pilha, é possível armazenar até 81 elementos (do tipo StkNode)
  • Os operadores ++ e -- não existem em Zig; usam-se += e -=
  • O ponteiro da pilha é um inteiro usado como índice do array stk
  • O ponteiro self não é passado explicitamente como parâmetro; ele é implicitamente assumido como ponteiro para a instância da pilha na qual a função é chamada
    • Em uma chamada como stack.pop(), self é um ponteiro para stack (semelhante a this em Java/C++)
  • A função init() é o construtor da pilha
  • As funções pop e push são "inlined"

Compilando e executando programas em Zig

Gerando um executável

  • Para gerar um executável, é necessária uma função main que represente o ponto de entrada do programa
  • Em programas simples, a função main pode ficar no mesmo arquivo
  • Para depuração independente de módulo, é possível inserir uma função main no final do arquivo e comentá-la após concluir a depuração
  • Comando de compilação: zig build-exe -O ReleaseFast program.zig

Executando blocos de teste de um módulo

  • Um dos melhores recursos de Zig, usado para testes e prototipagem
  • Um bloco de teste começa com test "message" { e termina com }
    • "message" é a string exibida durante a execução do teste
  • Blocos de teste são executados independentemente do executável, e o executável final não executa os testes
  • Comando de teste: zig test module.zig
  • O bloco de teste de example.zig testa as funções set e print; set recebe uma string decimal como parâmetro, e print exibe o cabeçalho "Input Grid" antes de imprimir o grid

Saída em Zig

  • A instrução std.debug.print chama a função print presente em debug.zig da biblioteca padrão std de Zig
  • O primeiro parâmetro é a string de formato, e o segundo é uma struct anônima contendo a lista de variáveis a exibir
  • Quando não há formato, a struct fica vazia
  • Por padrão, a saída vai para stderr
  • Ao contrário do printf em C, Zig pode processar strings literais e listas de variáveis em tempo de compilação

Depurando executáveis

  • Usar depurador não é simples fora de IDEs com depurador integrado (Eclipse, IntelliJ IDEA) ou kits de desenvolvimento integrados (w64devkit)
  • A integração de símbolos incha o código e exige compilação no modo Debug, gerando código executável muito menos eficiente
  • Zig oferece uma solução prática para evitar esses problemas

Função embutida @breakpoint

  • Ao inserir @breakpoint(); no código-fonte, o programa para nesse ponto quando executado em um depurador
  • É um recurso útil para depurar código Zig otimizado sem símbolos
  • Logo antes de @breakpoint();, é possível usar std.debug.print para exibir as variáveis que se deseja rastrear e verificar seus valores naquele momento
  • No exemplo debug_example.zig, são inseridos dentro da função set o código para imprimir o grid e as variáveis, além de @breakpoint();
  • Comando de build: zig build-exe debug_example.zig
  • Depois, chama-se debug_example.exe com um depurador como gdb e executa-se o programa com o comando r
  • Use o comando c para continuar e acompanhar o conteúdo do grid e o rastreamento das variáveis
  • Repetindo Enter para continuar, é possível confirmar que os valores do grid coincidem com o bloco de teste de example.zig

Programação de baixo nível em Zig

Representação da matriz

  • Os dígitos decimais são armazenados na matriz como inteiros padrão u8
  • O grid de entrada está em formato de string, mas os caracteres ASCII são convertidos internamente para inteiros u8
  • O armazenamento dos números é feito linearmente, linha por linha, no array grid de 81 posições: var grid = [_]u8{0} ** 81;
  • Para verificar a correção do grid, é preciso acessar os elementos por linha e coluna
  • Cria-se um array de 9 ponteiros, cada um apontando para o início de uma linha
  • Usa-se label break para retornar um valor de um bloco de código: break :fill9x9 m; inicializa a matrix com m
  • Notação de acesso a elementos: element = matrix[i][j]

Representando dígitos decimais com bits

  • A ideia central é substituir o dígito decimal inteiro i pelo inteiro code
    • i ∈ [1,9] → code = 2ⁱ⁻¹
    • i = 0 → code = 0
  • A posição em que o único bit de code é definido como 1 é i-1 (quando i está entre 1 e 9); caso contrário, todos os bits são 0
  • É fornecida uma tabela com os valores de code para cada número (1→1, 2→2, 3→4, ..., 9→256)

Calculando code em Zig

  • O valor de code é calculado com o operador de deslocamento à esquerda apenas quando c não é 0: code = @as(u9,1) << (c-1);
  • Em Zig, constantes precisam ter o tamanho apropriado para que a operação seja compilada e o resultado seja atribuído à variável
  • code é declarado com o tipo u9 (o valor máximo 256 exige no mínimo 9 bits)
  • Zig pode ter variáveis com tamanho arbitrário em bits
  • A função embutida @as faz cast da constante 1 para o tipo u9

Representação do grid com bit fields

grid com bit fields por linha

  • O array lines espelha todo o grid, representando cada linha como um inteiro de 9 bits: var lines = [_]u9{0} ** 9;
  • Ao acessar o array pela linha i, verifica-se com uma operação bitwise AND (&) se um determinado número já está naquela linha: lines[i] & code
  • Se o resultado for 0, o número ainda não está na linha i; caso contrário, é duplicado

grid com bit fields por coluna

  • O array columns espelha todo o grid, representando cada coluna como um inteiro de 9 bits: var columns = [_]u9{0} ** 9;
  • Ao acessar o array pela coluna j, verifica-se com uma operação bitwise AND se um determinado número já está naquela coluna: columns[j] & code
  • Se o resultado for 0, o número ainda não está na coluna j; caso contrário, é duplicado

Regras do Sudoku

  • Ao inserir um novo número em um grid de Sudoku vazio, ele não pode já existir na linha, coluna e célula correspondentes ao novo elemento
  • Uma célula é cada um dos 9 grids 3x3 separados por linhas grossas
  • Cada elemento específico do grid 9x9 pertence a uma linha, coluna e célula únicas que o contêm
  • No grid de exemplo, a primeira célula contém 3, 5, 6, 8 e 9, e faltam 1, 2, 4 e 7
  • Os arrays lines e columns lidam com a verificação de duplicatas em linhas e colunas
  • É necessário um novo array para verificar duplicatas nas células

grid com bit fields por célula

  • O array cells espelha todo o grid, representando cada célula como um inteiro de 9 bits: var cells = [_]u9{0} ** 9;
  • Fica mais fácil acessar cells como uma matriz 3x3
  • O array cell é preenchido de maneira semelhante ao que foi feito com a matriz 9x9
  • É preciso determinar a linha e a coluna da matriz cell a partir da linha e da coluna do elemento no grid 9x9 original
  • Como divisão inteira é muito lenta, usa-se o array cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; para fornecer o resultado da divisão
  • Ao acessar a matriz pela linha i e coluna j do elemento do grid 9x9, verifica-se com uma operação bitwise AND se um determinado número já existe na célula do elemento: cell[cindx[i]][cindx[j]] & code
  • Se o resultado for 0, o número ainda não está na célula; caso contrário, é duplicado

Teste de duplicidade de elementos

  • A verificação de duplicidade de um elemento é concluída combinando com bitwise OR (|) todos os elementos anteriores da mesma linha, coluna e célula e depois aplicando bitwise AND com o code do elemento
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {  
    unreachable;  
}  
  • Se o resultado for 0, o elemento ainda não existe na linha, coluna e célula
  • Se o resultado não for 0, o programa para ao executar o comando unreachable
  • É a forma mais simples em Zig de indicar explicitamente um erro de execução
  • O código real também exibe detalhes sobre a posição em que ocorreu o erro
  • Exemplo: se o 0 logo após o primeiro 8 na string de entrada for substituído por 5, ocorre erro porque já existe um 5 na linha 3 da coluna 1

Atualização das estruturas de dados

  • Na função set, um laço for duplo percorre as linhas e copia para o grid cada novo elemento da string de entrada s
    • A variável k mantém o índice do novo caractere de entrada na string s
  • O caractere é convertido para u4 (variável c) subtraindo '0'
  • Se o novo elemento a inserir no grid não for 0 (c != 0), o code calculado com a instrução de deslocamento à esquerda é copiado para cada grid espelho
    • Isso é feito com bitwise OR (|=) no grid espelho correspondente:
lines[i] |= code;  
columns[j] |= code;  
cell[cindx[i]][cindx[j]] |= code;  
  • Não é necessário testar explicitamente se c está entre 1 e 9, porque a operação de shift causará overflow ao ser executada
  • Exemplo: se o 0 logo após o primeiro 8 na string de entrada for substituído por :, ocorre erro de execução
  • O mesmo acontece se esse mesmo 0 for substituído por /
  • O programa só funciona quando os valores estão entre 1 e 9, ou seja, quando o grid de entrada contém apenas dígitos decimais
  • Muitos grids de Sudoku na web usam . em vez de 0, por isso a função set contém a linha if (s[k] == '.') c = 0;
  • Isso contorna convenientemente a operação de shift, já que o valor de c é 0

Prototipagem e robustez

  • Os erros forçados nas duas seções acima demonstram um recurso importante de Zig
  • Um deles é a robustez de Zig — no caso da operação de shift, comportamento incorreto não é permitido e é capturado em tempo de execução
  • Embora todo o esforço pareça voltado à eficiência, é um caso típico em que desempenho é trocado por robustez
  • Em C, se a operação de shift perde bits, isso é problema do programador, e isso se converte em melhor desempenho de certas instruções de assembly
  • Outro recurso é a possibilidade de usar blocos de teste para prototipagem
  • As aplicações possíveis são inúmeras, e a mostrada aqui é apenas depurar uma situação específica quando ocorre um erro
  • Só esses recursos já oferecem capacidades surpreendentes, muito raras em linguagens de programação, especialmente em linguagens compiladas

Conclusão

  • Zig é composto por três elementos centrais: compatibilidade com C, compilação cruzada e instalação simples
  • Essas características mostram o potencial de se tornar um novo padrão para linguagens de programação de sistemas
  • Muitas vantagens antes encontradas apenas em linguagens interpretadas estão gradualmente migrando para linguagens compiladas para oferecer melhor desempenho
  • Em Zig, a semelhança com linguagens interpretadas é especialmente marcante por causa do conceito de execução em tempo de compilação
  • Isso é ao mesmo tempo o que torna Zig especialmente diferente e poderoso, e também o que pode torná-lo difícil de entender

1 comentários

 
GN⁺ 2025-11-08
Opiniões do Hacker News
  • Este texto começa afirmando que “Zig não é apenas uma linguagem simples, mas uma forma completamente nova de programar”, mas na prática quase não aborda recursos realmente próprios do Zig
    Inferência de tipos, structs anônimas, labeled break e afins já existem em outras linguagens há muito tempo
    O que de fato é único é o comptime, mas isso nem chega a ser mencionado
    Não é um conceito totalmente novo como macros de Lisp, mas a forma como o Zig usa isso no lugar de genéricos é interessante
    Ainda assim, a tese do texto soa bastante exagerada

    • Rust também poderia ser descrita como uma “forma completamente nova”
      Em Rust, dá para expressar claramente o momento em que o código é executado, e a ideia de um projeto tipo motor de consultas que explora todo o espaço do código é marcante
    • A linguagem D já oferece execução de funções em tempo de compilação desde 2007
      Veja a documentação do D
      Se for uma const-expression, a execução acontece automaticamente
    • Já não faz sentido agrupar C/C++ como se fosse uma coisa só
      São linguagens tão diferentes quanto Java e Scala
    • comptime não é uma invenção mágica, mas uma versão moderna de metaprogramação
      O Zig é mais limpo que templates de C++, mas parece mais uma alternativa prática do que algo revolucionário
      Pessoalmente, não entendo esse entusiasmo excessivo, como aconteceu com Rust
    • Ao ler “forma completamente nova”, eu esperava um novo paradigma como LISP ou Prolog, mas não havia nada disso
      Li toda a documentação do Zig e mesmo assim não encontrei nada realmente surpreendente, o que me deixou confuso
  • O maior problema do Zig é que não dá para anexar dados aos erros
    Erros são passados apenas por um canal auxiliar, o que dificulta o debugging e acaba levando desenvolvedores a omitirem dados de erro
    Veja a issue relacionada
    Só com um código simples como AccessDenied fica difícil saber a causa

    • Li o texto do matklad, e a abordagem de separar códigos de erro de informações de diagnóstico me pareceu convincente
      Na prática, mesmo usando objetos Error complexos, muitas vezes ainda é preciso um canal de diagnóstico separado
    • Em linguagens de sistema, anexar dados aos erros nem sempre é a melhor ideia
      Por causa do overhead de desempenho ou de problemas no estado do sistema, em alguns casos é mais seguro tratar isso com binding tardio
      O Zig adota uma filosofia que prioriza essa precisão e determinismo
    • No Zig também está em discussão a possibilidade de incluir informações definidas pelo usuário em stack traces de erro
      Veja a issue relacionada
      Mas o que realmente faz falta é logging estruturado e rastreamento de contexto com base na pilha de chamadas
    • std.zon é citado como um bom exemplo, e há um movimento na comunidade para reunir vários padrões de tratamento de erro e refletir isso no padrão
    • Impedir que dados sejam anexados aos erros pode, na verdade, incentivar um design de erros mais claro
      Isso evita que desenvolvedores preguiçosos simplesmente saiam grudando dados em tudo
  • Concordo com a afirmação de que o próprio modo de desenvolvimento do Zig é uma nova forma de desenvolver linguagens
    O processo lento de evolução, com análise cuidadosa dos recursos e remoção do que é desnecessário, é marcante

    • Mas esse tipo de abordagem também é comum em Java, Rust e outras
      Eu gostaria de ouvir de forma mais concreta o que exatamente há de único no Zig
  • Gosto do fato de que dá para instalar o Zig pelo PyPI
    Se você instalar o pacote ziglang com pip install ziglang, já pode usar na hora
    Também dá para compilar código C usando uvx

    • Como um wheel do Python pode empacotar software arbitrário, esse tipo de instalação se torna possível
    • Mas essa abordagem parece uma reinvenção mais incômoda do nix
    • Eu gostaria que o Nim também tivesse uma opção de instalação assim
    • Pessoalmente, acho micromamba ou pixi formas melhores de gerenciar pacotes do que pip/uv
    • Graças às ferramentas de IA, agora ficou muito mais fácil aprender qualquer linguagem
  • É uma pena ver recursos que já existiam em linguagens como Ada, Object Pascal e Modula-2 sendo apresentados como “inovação” do Zig
    É interessante como, ao serem reembaladas com sintaxe no estilo C, ideias de 40 anos atrás passam a parecer novas

  • A introdução do texto era boa, mas depois ele acaba virando apenas uma enumeração de recursos do Zig
    A sintaxe intuitiva e o fluxo de controle explícito do Zig (defer etc.) são atraentes
    Graças ao comptime, também não é preciso aprender uma sintaxe separada de macros

    • O verdadeiro charme do Zig está em um projeto sem redundâncias desnecessárias
      Tudo se encaixa naturalmente, a ponto de, mesmo no primeiro uso, parecer uma ferramenta usada há muito tempo
    • Vale a pena ler também a análise da sintaxe do Zig do matklad
  • A sintaxe for (0..9) do Zig é intuitiva, mas por ser um intervalo aberto, às vezes confunde
    Como no range(0, 9) do Python, é fácil esquecer se o último valor está incluído ou não

    • Em Rust, a distinção entre 0..9 e 0..=9 deixa isso claro
    • A consistência de usar apenas intervalos semiabertos como no Zig, por outro lado, reduz erros
      O tamanho do intervalo vira simplesmente a diferença, e percorrer em ordem reversa também fica mais simples
    • Odin diferencia isso de forma mais explícita com 0..<5 (aberto) e 0...5 (fechado)
  • Não gosto das regras de identificadores do Zig
    A mistura de snake_case e camelCase parece estranha
    Ainda assim, o sistema de build, o alocador de memória e a experiência de compilação são excelentes
    Eu uso Rust principalmente, mas continuo curioso sobre o Zig

    • Eu também sou assim. Pessoalmente, não sigo a convenção de nomenclatura de funções privadas
      A regra de prefixos em bibliotecas C também me incomoda do mesmo jeito
  • O apelo do Zig não está em um único recurso, mas no acúmulo de decisões práticas
    Escolhas que no começo pareciam radicais vão fazendo sentido conforme o entendimento se aprofunda
    O Zig é uma linguagem que recompensa desenvolvedores curiosos

    • Fiz um joguinho em Odin e foi uma experiência realmente divertida
  • Um dos motivos pelos quais o Zig é bom é que ele reconhece a realidade do código de sistema de baixo nível
    Muitas linguagens ignoram isso por razões estéticas, mas o Zig não faz isso

    • Se você navegar pelas definições da biblioteca padrão, verá diretamente até o tratamento de casos especiais como o Plan9 OS
      Veja a documentação do page_allocator
    • Ainda assim, faltam exemplos concretos para sustentar esse tipo de afirmação