Aprendiendo C3
(alloc.dev)- C3 está basado en el lenguaje C y ofrece funciones avanzadas como módulos, sobrecarga de operadores, genéricos y ejecución en tiempo de compilación
- Mantiene la sintaxis familiar de C, pero incorpora sintaxis que refuerza la productividad y la estabilidad como manejo de errores, defer y foreach
- La introducción de contratos declarativos, tipos opcionales y mecanismos de manejo de errores mejora la seguridad y la claridad
- Se ofrece un entorno de desarrollo práctico con biblioteca estándar e integración del sistema de build, además de asignación de memoria temporal
- Tiene similitudes con el lenguaje Zig en aspectos como build, creación de proyectos y estructura del código, y deja ver nuevos experimentos de diseño de lenguaje
Resumen y características de C3
¿Qué es C3?
- C3 es un lenguaje construido sobre el C existente, que mantiene una sintaxis familiar y al mismo tiempo ofrece funciones difíciles de lograr en C, como sistema de módulos, sobrecarga de operadores, genéricos, ejecución en tiempo de compilación, manejo de errores, defer, value methods, contratos graduales, slices, foreach y soporte de tipos dinámicos
- Usa una estructura de módulos con namespaces para evitar colisiones de nombres (
abc::Context, con namespaces imperativos) - Su objetivo principal es aumentar la productividad y ofrecer funciones modernas de programación de sistemas de forma segura
Características del lenguaje
Ejemplo Hello World
- Sintácticamente es similar a C
- En la declaración de funciones se debe usar explícitamente la palabra clave
fn - Las funciones de la biblioteca estándar para entrada/salida son potentes, y permiten imprimir directamente varios tipos
Bucle foreach
- A diferencia de C, incluye soporte nativo para sintaxis foreach
- En los bucles por referencia se escribe
&antes del nombre de la variable (función avanzada) - Soporta
breakycontinue, y es similar al foreach de otros lenguajes
Bucle while
- Antes de C99 no se podían usar declaraciones dentro de la condición de
while, pero en C3 sí es posible
enum y sentencia switch
- La sentencia
switchadmite break implícito (la mezcla debreakimplícito y explícito puede generar opiniones divididas) - Con la palabra clave
nextcasese admite un salto explícito entre casos (facilita implementar tablas de salto) - Permite controlar de forma concisa flujos
switch-caseque en lenguajes como Zig o C eran más complejos
Palabra clave defer
- Al salir del scope, las sentencias programadas con defer se ejecutan en orden inverso, garantizando de forma segura la limpieza de recursos
- Puede usarse
deferjunto concatchytrypara controlar el flujo de manejo de errores
struct y union
- Dentro de un
structse permiten sub-structs/unions con nombre o anónimos, lo que facilita diseñar patrones de tagged union - Se distingue de forma estricta entre formas anónimas (duplicación de campos con el mismo nombre) y colisiones de nombres
Manejo de errores
- El símbolo
?brinda soporte para tipos opcionales, unificando errores y valores opcionales para mayor comodidad - La palabra clave
catchpermite bifurcar cuando hay estado vacío (sin Optional) o error - A diferencia de Rust y Zig, la distinción entre error y valor opcional es más débil (ventaja: simplicidad, desventaja: menor claridad conceptual)
- El operador
!(rethrow) permite propagar excepciones
Contratos (Contracts)
- Las precondiciones y postcondiciones de una función (Require/Ensure) se escriben entre
<* .. *>(las condiciones se verifican al compilar) - También admite análisis de fold en tiempo de compilación (el análisis estático aún no está implementado)
Métodos de struct
- Los métodos asociados se definen con especificación de tipo + notación con punto (
Foo.next), con namespace incluido (también para primitivos) - Se permiten métodos en todo tipo de tipos, como structs, unions y enums
Macros
- Macros basadas en evaluación en tiempo de compilación (palabra clave
macro) - Usa
$para parámetros de tiempo de compilación y#para pasar valores antes de evaluar - Estilo C (minimiza problemas de macros enredadas, enfatiza estabilidad del AST, verificación con prefijo
@, etc.) - La reflexión de tipos y la ejecución en tiempo de compilación se gestionan mediante macros
Propiedades de tipos
alignof, kindof, extnameof, sizeof, typeid, methodsof, has_tagof, tagof, is_eq, is_ordered, is_substruct, etc.- Son adecuadas para metaprogramación y reflexión
Literales Base64/Hex
- Permite declarar directamente secuencias de bytes con formas como
b64"..."yx"..." - La necesidad puede reemplazarse con la macro integrada
$embed(en la práctica se usa poco)
Tipos primitivos
- Incluye varios tipos básicos como int, uint, char (siempre unsigned), bool, float, int128/uint128, etc.
- Tiene tipos separados para punteros/tamaños como iptr, uptr, isz y usz (algo menos intuitivos)
- A diferencia de C, garantiza el tamaño en bits
Otros
- Incluye un conjunto amplio de funciones como sobrecarga de operadores, subtipado de structs, genéricos, runtime dispatch, tipo any y bitstructs
Práctica: experiencia usando C3
Instalación de C3
- Soporta tanto binarios precompilados del sitio oficial como compilación directa desde el código fuente
- Requiere instalar LLVM y LLD (si hay problemas de enlace, usar los flags de CMake
-DLLVM_DIRy-DLLD_DIR) - Debido a que algunas distribuciones no incluyen la biblioteca LLD, se recomienda descargar directamente el binario
- El compilador de C3 requiere dependencia de
libtinfo
Creación de proyecto
- El comando
c3c initgenera la estructura estándar de carpetas (LICENSE/README.md/project.json/src, etc.) - Configura la base del proyecto con Build, targets de compilación y ajustes de fuentes (similar a Zig o Cargo)
- El archivo
main.c3por defecto es muy conciso (opinión: adecuado para nuevos usuarios)
Crear una calculadora
Diseño y objetivo
- Se practica la implementación de un parser descendente recursivo (Recursive Descent Parser) y la lógica central de una calculadora, usando en C3 varias construcciones como funciones, entrada/salida, gestión de memoria y bucles
- El objetivo es identificar directamente qué tan intuitiva es la sintaxis y cuáles son sus puntos fuertes o incómodos en productividad real
Procesamiento de entrada
- Usa
@poolpara un asignador temporal (tmem), con liberación automática de memoria al salir del scope (arena allocator) - Soporta
tmem(temporal) ymem(general) como esquema estándar de gestión de memoria, además del patrón de pasar allocators por función (mezcla ventajas de Zig y C) - La función
maindebe declarar siempre su valor de retorno (el compilador lo exige) - Las funciones cuyo valor de retorno puede ignorarse sin problema se marcan con el atributo
@maydiscard(evita ignorarlas maliciosamente)
Implementación del tokenizer
- Descompone la entrada del usuario en una lista de tokens
- Aprovecha varias construcciones de control del lenguaje, como
List, sintaxisforeachyswitch-case(nextcase, combinación debreakimplícito/explicito) de la biblioteca estándar de C3 - Hay cierta confusión con la sintaxis de slices (ambos índices extremos incluidos) y con los slices de longitud 0 (existe una sintaxis separada para especificar longitud)
- La combinación de asignadores temporales/generales muestra transparencia y flexibilidad en la gestión de memoria, superior a la de lenguajes como Rust
Implementación del parser
- Experiencia de codificación directa del parser (omitida)
Conclusión y opinión general
- C3 busca el punto de encuentro entre los lenguajes de sistemas tradicionales y el diseño moderno
- Fue diseñado estudiando Zig, Rust y C, como un lenguaje que busca compatibilizar rendimiento y estabilidad del código
- Destacan funciones como modularidad, manejo seguro de memoria/errores/contratos, metaprogramación potente y sistema de build intuitivo
- La curva de aprendizaje permite una entrada gradual para quienes ya tienen experiencia con C
- Aún necesita mejorar en puntos como el ecosistema inmaduro de language server e IDE, y ciertas decisiones sintácticas que pueden gustar o no
- Vale la pena seguirlo como un posible lenguaje alternativo de próxima generación para desarrollo low-level y de sistemas
Aún no hay comentarios.