Aprendendo C3
(alloc.dev)- C3 é baseado na linguagem C e oferece recursos avançados como módulos, sobrecarga de operadores, genéricos e execução em tempo de compilação
- Mantendo a sintaxe familiar de C, incorpora recursos que reforçam produtividade e segurança, como tratamento de erros, defer e foreach
- A introdução de contracts declarativos e de tipos opcionais com um modelo próprio de tratamento de erros melhora a segurança e a clareza
- Há suporte a um ambiente de desenvolvimento prático, com biblioteca padrão, integração com o sistema de build e alocação temporária de memória
- Em build, criação de projetos e estrutura de código, há semelhanças com a linguagem Zig, sugerindo novos experimentos de design de linguagem
Visão geral e características do C3
O que é C3?
- C3 é uma linguagem construída sobre a linguagem C, preservando uma sintaxe familiar e, ao mesmo tempo, oferecendo recursos difíceis de obter em C, como sistema de módulos, sobrecarga de operadores, genéricos, execução em tempo de compilação, tratamento de erros, defer, value methods, contracts graduais, slices, foreach e suporte a tipos dinâmicos
- A estrutura de módulos usa namespaces para evitar conflitos de nomes (
abc::Context, por exemplo, com namespaces explícitos) - O objetivo principal é aumentar a produtividade e oferecer recursos modernos de programação de sistemas com segurança
Características da linguagem
Exemplo Hello World
- É sintaticamente semelhante a C
- Na declaração de funções, é obrigatório usar explicitamente a palavra-chave
fn - As funções da biblioteca padrão para entrada e saída são poderosas, e vários tipos também podem ser impressos diretamente
Loop foreach
- Diferentemente de C, oferece suporte nativo à sintaxe foreach
- Em laços com referência, usa-se
&antes do nome da variável (recurso avançado) - Suporta break e continue, de forma semelhante ao foreach de outras linguagens
Loop while
- Antes do C99 não era possível declarar variáveis dentro da condição do while, mas em C3 isso é permitido
enum e switch
- O switch oferece suporte a break implícito (a mistura de break implícito e explícito pode dividir opiniões)
- A palavra-chave
nextcasepermite mover-se explicitamente entre casos (facilitando a implementação de tabelas de salto) - É possível controlar o fluxo de switch-case de forma mais concisa do que em linguagens anteriores como Zig e C, onde isso era mais complexo
Palavra-chave defer
- Ao final do escopo, os blocos reservados com defer são executados em ordem reversa, garantindo com segurança a liberação de recursos
- Também há uso de defer em combinação com
catchetry(controle de fluxo de tratamento de erros)
struct e union
- Dentro de structs, são permitidos sub-structs/unions nomeados ou anônimos, o que facilita projetar o padrão tagged union
- A distinção entre anônimos (incluindo duplicação de campos com o mesmo nome) e conflitos de nome é tratada de forma rigorosa
Modelo de tratamento de erros
- O símbolo
?oferece suporte a tipos opcionais, unificando erros e valores opcionais para maior conveniência - Com a palavra-chave
catch, é possível ramificar em caso de estado vazio (sem Optional) ou erro - Diferentemente de Rust e Zig, a distinção entre erro e valor opcional é mais fraca (vantagem: simplicidade; desvantagem: menor clareza de intenção)
- O operador
!(rethrow) permite propagar exceções
Contracts
- As pré e pós-condições de funções (Require/Ensure) são escritas entre
<* .. *>(as condições são verificadas na compilação) - Há suporte até para análise de fold em tempo de compilação (análise estática ainda não implementada)
Métodos em struct
- Usa métodos associados com anotação de tipo + notação por ponto (
Foo.next), com namespace incluso (inclusive para tipos primitivos) - Permite métodos em todos os tipos, como struct, union e enum
Macros
- Macros baseadas em avaliação em tempo de compilação (palavra-chave
macro) $implementa parâmetros de tempo de compilação, e#permite passagem antes da avaliação- Estilo C (minimizando problemas de macros entrelaçadas, enfatizando estabilidade da AST, checagens com prefixo @ etc.)
- Reflexão de tipos e execução em tempo de compilação são tratadas por macros
Propriedades de tipo
alignof, kindof, extnameof, sizeof, typeid, methodsof, has_tagof, tagof, is_eq, is_ordered, is_substructetc.- Adequadas para metaprogramação e reflexão
Literais Base64/Hex
- Permite declarar sequências de bytes diretamente no formato
b64"..."ex"..." - Isso pode ser substituído pela macro embutida
$embed(na prática, o uso é raro)
Tipos primitivos
- Inclui vários tipos básicos como int, uint, char (sempre unsigned), bool, float, int128/uint128 etc.
- Há tipos separados para ponteiros/tamanho, como iptr, uptr, isz e usz (um pouco menos intuitivos)
- Diferentemente de C, o tamanho em bits é garantido
Outros
- Traz um conjunto amplo de recursos como sobrecarga de operadores, subtipagem de structs, genéricos, runtime dispatch, tipo any e bitstructs
Prática: experiência com C3
Instalação do C3
- Suporta tanto binários pré-compilados do site oficial quanto build direto do código-fonte
- É necessário instalar LLVM e LLD (em caso de problema de linkedição, usar as flags de CMake
-DLLVM_DIRe-DLLD_DIR) - Devido a problemas em algumas distribuições que não incluem bibliotecas do LLD, recomenda-se baixar os binários diretamente
- O compilador C3 exige dependência de
libtinfo
Criação de projeto
- O comando
c3c initgera a estrutura padrão de pastas (LICENSE/README.md/project.json/srcetc.) - Organiza a base do projeto com Build, alvos de build e configurações de código-fonte (semelhante a Zig e Cargo)
- O arquivo main.c3 padrão é bastante enxuto (opinião: adequado para novos usuários)
Criando uma calculadora
Design e objetivo
- Implementar um parser recursivo descendente (Recursive Descent Parser) e a lógica central de uma calculadora para praticar vários recursos do C3, como funções, entrada/saída, gerenciamento de memória e laços
- O objetivo é entender diretamente os pontos fortes e incômodos da linguagem, como a intuitividade da sintaxe e a produtividade no uso real
Tratamento de entrada
- Usa alocador temporário (tmem) com @pool, liberando memória automaticamente ao final do escopo (arena allocator)
- O modelo padrão de gerenciamento de memória oferece tmem (temporário) e mem (geral), além de um padrão de passar alocadores por função (misturando vantagens de Zig e C)
- A função main deve declarar explicitamente o valor de retorno (exigido pelo compilador)
- Funções cujo retorno pode ser ignorado recebem o atributo @maydiscard (evitando ignorar resultados de forma indevida)
Implementação do tokenizer
- Decompõe a entrada do usuário em uma lista de tokens
- Aproveita vários controles de fluxo da biblioteca padrão do C3, como List, sintaxe foreach e switch-case (
nextcase, combinação de break implícito/explícito) - Há alguma confusão com a sintaxe de slices (os índices de ambas as pontas são inclusivos) e com slices de comprimento zero (há uma sintaxe separada para especificar o tamanho)
- O uso misto de alocadores temporários/gerais mostra transparência e flexibilidade no gerenciamento de memória, com vantagens em relação a linguagens como Rust
Implementação do parser
- Relato de experiência prática ao escrever o parser diretamente (omitido)
Conclusão e opinião geral
- C3 busca o ponto de encontro entre linguagens de sistema tradicionais e design moderno
- Foi projetada estudando Zig, Rust e C, com a meta de conciliar desempenho e estabilidade do código
- Destacam-se recursos como modularidade, tratamento seguro de memória/erros/contracts, metaprogramação poderosa e sistema de build intuitivo
- A curva de aprendizado é gradual para quem já tem experiência com C
- O ecossistema ainda é imaturo, com language server, IDE e outros pontos a melhorar, além de algumas escolhas de sintaxe que podem dividir opiniões
- Vale a atenção como uma linguagem alternativa de próxima geração para desenvolvimento low-level e de sistemas
Ainda não há comentários.