Central Docs
Catálogo Inicio

Especificadores y Calificadores

Los calificadores modifican la forma en que se puede usar un tipo o un valor. Los especificadores modifican una declaración completa, su visibilidad o la forma en que el compilador debe tratarla.

En un lenguaje de bajo nivel esto es importante porque una declaración no solo describe qué datos existen: también puede limitar quién los modifica, indicar si una dirección representa hardware y definir cómo se organizan los bytes y los bits en memoria.

  • Un calificador impone una regla de uso, como const.
  • Un especificador expresa una propiedad de una declaración, como inline o volatile.
  • Un decorador añade metadatos con @, como @packed o @unsafe.

Estas palabras no cambian arbitrariamente el tipo de un valor. Hacen explícitas las garantías que el compilador debe conservar.

Calificadores de tipo

const

const declara que el valor no puede modificarse después de su inicialización. El compilador rechaza cualquier asignación posterior. Se escribe delante del tipo:

limite: const int32 = 10
texto: const char[6] = "Hola"

En los punteros, la constancia del puntero y la del valor apuntado son independientes:

const puntero *: int32
apuntado *: const int32
const referencia *: const int32
  • const dato *: int32: el puntero no puede cambiar de destino; el valor sí.
  • dato *: const int32: el puntero puede cambiar de destino; el valor apuntado no.
  • const referencia *: const int32: ni el destino ni el valor apuntado pueden modificarse.

Un puntero es una variable que contiene una dirección de memoria. Por eso estas dos formas controlan cosas distintas: una puede proteger la dirección guardada y la otra protege los datos que existen en esa dirección. const no convierte los datos en secretos ni impide que otro puntero válido los cambie; solo establece una garantía para ese acceso.

chain ya es inmutable por definición y no puede declararse como const chain. Len_t y Size_t también son parámetros expansivos de compilación y no aceptan const.

Especificadores de declaración

Visibilidad

public y private controlan si una declaración puede usarse desde fuera de su ámbito. El ámbito es la parte del programa donde un nombre es visible, como un módulo o una clase.

Especificador Forma Función
public public struct Punto: Hace pública la declaración.
private private func auxiliar() void: Restringe la declaración a su ámbito.

Pueden aplicarse a variables, funciones, struct, mold, flags, enum y class.

Si no se escribe ninguno, la declaración es pública por defecto.

inline

inline es un especificador de funciones:

inline func sumar(a: int32, b: int32) int32:
    return a + b

Una llamada normal salta a otra dirección y vuelve después de ejecutar la función. En prod y high, Bagron emite alwaysinline y ejecuta su pasada AlwaysInliner antes de las demás optimizaciones: la expansión solicitada por inline ocurre desde el backend de Bagron, no queda a criterio exclusivo del modelo de costes de LLVM. En dev se conserva inlinehint para inspeccionar el IR histórico sin expandirlo.

inline puede aumentar el tamaño del código y debe reservarse para funciones pequeñas o rutas calientes. No cambia la firma, el tipo de retorno ni las reglas de seguridad de la función; las declaraciones externas y los cuerpos ASM no se expanden.

Acceso explícito a datos globales

Las funciones y los métodos tienen visibilidad local para los datos: los parámetros y variables locales están disponibles normalmente, pero una variable declarada en el módulo debe solicitarse de forma explícita:

contador: int32 = 0

func siguiente() int32:
    global contador
    contador += 1
    return contador

global solo acepta una variable de módulo que ya exista. La declaración es válida para toda la función aunque aparezca dentro de un bloque, y no crea instrucciones ni almacenamiento adicional en LLVM. Una función puede seguir llamando a otras funciones —incluidas extern— sin escribir global, porque las funciones pertenecen a un espacio de nombres distinto de los datos.

El acceso implícito a una variable global produce un error de compilación. Esto evita que una función adquiera estado compartido accidentalmente y hace visible en el código cada dependencia mutable del módulo. main conserva la misma regla; su privilegio especial es únicamente ser el punto de entrada.

Especificadores de almacenamiento

volatile

El compilador normalmente puede reutilizar una lectura, eliminar una escritura que parezca innecesaria o reorganizar accesos ordinarios. volatile le indica que cada lectura y cada escritura del agregado representa una interacción real con memoria externa y debe conservarse.

volatile se aplica a agregados completos:

volatile struct Registro:
    estado: uint32

volatile mold Vista:
    valor: uint32

volatile flags Estado uint8:
    activo: bool = false
    reservado: uBint<7> = 0

Los accesos de lectura y escritura al almacenamiento del agregado se conservan como operaciones volátiles. Esto es útil para memoria mapeada, registros de hardware o memoria compartida. Un patrón típico para un bloque MMIO usa un puntero a un agregado volátil y exige desreferenciarlo explícitamente:

volatile struct UART:
    status: uint32
    control: uint32

@unsafe
func leer_status(registros *: UART) uint32:
    return (registros).status

volatile no hace una operación atómica ni reemplaza mutexes, barreras o un protocolo de sincronización. Solo garantiza que el acceso de memoria no se elimine ni se trate como una lectura o escritura ordinaria.

Decoradores de funciones

Las funciones solo admiten decoradores de nombre simple, sin argumentos, y no se pueden repetir. Un decorador desconocido produce un error de compilación.

Decorador Forma Función
unsafe @unsafe Permite operaciones que cruzan la frontera insegura, como indexación dinámica o acceso a punteros crudos.
assembler @assembler Declara una función con cuerpo de ensamblador preparado mediante #prep assembler. También implica unsafe.

Una función segura no puede llamar a una función @unsafe o @assembler. @unsafe debe reservarse para la pequeña frontera que realmente necesita operaciones sin garantía automática; el resto del programa puede permanecer en contexto seguro. Una función @assembler puede tener un único bloque #prep; las funciones con retorno distinto de void deben terminarlo con return asm, mientras que una función void puede terminar directamente al final del bloque ASM. También puede existir sin bloque #prep, en cuyo caso solo admite un return normal.

El dialecto gas-intel usa la sintaxis Intel del ensamblador integrado de GAS/LLVM; no es sintaxis NASM. intel se conserva como alias de compatibilidad. El dialecto att usa la sintaxis AT&T. Clang valida cada bloque durante la compilación y Bagron remite sus diagnósticos al archivo .bg y a la función @assembler que los originó.

Decoradores de agregados

Estos decoradores se aplican a struct, mold y flags según la tabla de compatibilidad de Estructuras.

Decorador Forma Función
packed @packed Reduce o elimina el padding automático del layout.
align @align(N) Solicita una alineación explícita; N debe ser una potencia de dos entre 1 y 64.
order_lsb @order_lsb En flags, asigna los campos desde los bits menos significativos.
order_msb @order_msb En flags, asigna los campos desde los bits más significativos.

@order_lsb y @order_msb son alternativas del orden de bits y no deben usarse simultáneamente.

Decoradores de class

Las clases admiten @frozen:

@frozen
class Punto:
    func __init__(x: int32) void:
        self.x = x

Una clase congelada solo puede escribir sus atributos durante __init__ o __frontend__. Las escrituras posteriores, incluidas las compuestas y las realizadas mediante un alias de puntero que el compilador pueda demostrar, producen un error de compilación. La congelación es superficial: un objeto contenido en un campo puede seguir siendo mutable si su propia clase no es @frozen.

La propiedad se mantiene en toda la jerarquía: una clase @frozen solo puede heredar de otra @frozen, y una clase hija de una clase @frozen también debe ser @frozen.

Cuándo usar cada decorador

@packed

El compilador suele insertar padding para que cada campo respete la alineación natural de su tipo. @packed reduce ese espacio y hace que el layout sea más compacto y predecible. Es útil al describir formatos binarios, registros de hardware o estructuras compatibles con una ABI externa.

El coste es que algunos campos pueden quedar desalineados. Debe usarse solo cuando el layout compacto sea parte del contrato; no es una optimización general para cualquier estructura.

@align(N)

Solicita que el agregado comience con una alineación concreta. N debe ser una potencia de dos entre 1 y 64:

@align(8)
struct Bloque:
    valor: int64

Sirve para cumplir contratos de hardware, ABI o instrucciones que requieren direcciones alineadas. No cambia el orden de los campos ni convierte un tipo en otro.

@order_lsb y @order_msb

Estos decoradores solo determinan desde qué extremo del contenedor se asignan los campos de flags. No son una conversión de endianess y no cambian por sí solos el orden de bytes de un entero.

Verificación de volatile

El compilador cuenta con pruebas automatizadas para confirmar que volatile se conserva en lecturas y escrituras de struct, mold y flags, incluidos los bitfields de un bit. También se prueba el acceso directo, el acceso a registros MMIO mediante punteros con desreferenciación explícita y la diferencia frente a agregados no volátiles.

Estas pruebas verifican el LLVM generado: los accesos volátiles se emiten como operaciones load volatile y store volatile, mientras que los agregados normales conservan operaciones de memoria ordinarias.