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
inlineovolatile. - Un decorador añade metadatos con
@, como@packedo@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.