El lenguaje Ada: historia, instalación de GNAT y primeros programas
El lenguaje Ada: historia, instalación de GNAT y primeros programas
En el capítulo 11 terminamos de recorrer la parte del sistema operativo que se ocupa de la persistencia: cómo un conjunto de bloques sin significado se convierte en archivos con nombre, cómo el inodo separa los metadatos del contenido, cómo el journal protege la consistencia frente a un corte de energía y qué ocurre exactamente entre una llamada a write y el momento en que los datos tocan el disco. Con eso cerramos el bloque de mecanismos: procesos, memoria, dispositivos y almacenamiento.
Desde este capítulo cambiamos el punto de observación. Hasta ahora describimos qué hace el sistema operativo; a partir de aquí vamos a escribir programas que ejerciten esos mecanismos desde arriba, y para eso el curso usa dos lenguajes con filosofías muy distintas: Ada y Elixir. Elixir nos servirá para razonar sobre concurrencia por paso de mensajes y tolerancia a fallos, que es la vista que un runtime construye sobre los procesos del sistema. Ada nos servirá para lo contrario: mirar el software de sistemas desde el lado del compilador, donde el tipo de un dato y el rango de un valor son verificados antes de que exista un ejecutable.
Este capítulo es la puerta de entrada a Ada. No asume que hayas escrito una línea de Ada antes, pero tampoco se queda en el “hola mundo”: vamos a ver por qué el lenguaje existe, cómo se instala el compilador, qué pasa físicamente cuando compilas, cómo se organizan los archivos de un proyecto, y tres programas completos explicados instrucción por instrucción. Al final vas a poder leer código Ada ajeno y compilar el tuyo sin depender de un entorno gráfico.
Por qué un curso de sistemas operativos usa Ada
Hay una razón práctica y una razón conceptual.
La práctica es que Ada sigue vivo exactamente en el terreno que estudia este curso: software que controla hardware, que corre durante años sin reiniciarse y donde un error de rango no produce un ticket sino un accidente. Sistemas de control de vuelo, señalización ferroviaria, control de tráfico aéreo, dispositivos médicos y sistemas espaciales siguen escribiéndose en Ada porque el proceso de certificación de esos productos exige demostrar propiedades del código, y Ada fue diseñado para que esas propiedades se puedan demostrar.
La conceptual es más interesante para nosotros. Un sistema operativo es, en el fondo, una máquina de imponer restricciones: este proceso no puede tocar la memoria de aquel, este usuario no puede abrir ese archivo, este descriptor no puede recibir más de tantos bytes. El kernel impone esas restricciones en tiempo de ejecución, pagando el costo de una verificación en cada operación. Ada plantea la misma idea desplazada en el tiempo: gran parte de esas restricciones se declaran en el código y las verifica el compilador antes de ejecutar. Ver los dos enfoques en paralelo hace evidente algo que en C queda escondido: toda garantía se paga en algún momento, y la pregunta de diseño es dónde.
Los cuatro rasgos que definen al lenguaje y que vamos a ir viendo a lo largo de los próximos capítulos son estos:
- Legibilidad por encima de brevedad. Ada fue diseñado partiendo del supuesto de que un programa se escribe una vez y se lee muchas, incluso impreso en papel durante una auditoría. Por eso las palabras clave son palabras (
begin,end,procedure,loop), los bloques se cierran nombrando lo que cierran (end Hola;) y no existe la sintaxis ambigua. - Tipado fuerte y explícito. Dos tipos numéricos distintos no se mezclan aunque ambos sean números reales; hay que convertir a mano. Un tipo puede llevar su rango válido incorporado, y salirse de él es un error del programa, no un valor extraño que se propaga.
- Concurrencia dentro del lenguaje. Las tareas y los objetos protegidos son construcciones del lenguaje, no una biblioteca. El perfil Ravenscar restringe ese modelo a un subconjunto analizable para sistemas de tiempo real.
- Continuidad. Las revisiones del estándar —Ada 83, 95, 2005, 2012 y 2022— mantuvieron un grado alto de compatibilidad hacia atrás, hasta el punto de que código de los años ochenta suele compilar con herramientas actuales.
Historia: de dónde sale el lenguaje
La historia de Ada es útil por sí misma porque muestra cómo se diseña un lenguaje cuando el cliente no es un programador individual sino una organización con un problema de costos.
El problema de 1974
A mediados de los setenta, el Departamento de Defensa estadounidense hizo un inventario del software que estaba pagando y encontró dos cifras incómodas. La primera: la mayor parte del gasto se iba en sistemas embebidos, es decir, computadores dentro de aviones, barcos, radares y misiles, no en los grandes computadores administrativos. La segunda: ese software estaba escrito en cientos de lenguajes y dialectos distintos, muchos de ellos creados para un solo proyecto y mantenidos por un solo proveedor.
La consecuencia directa era duplicación. Cada lenguaje necesitaba su compilador, su depurador, su documentación y su gente capacitada. Un ingeniero que cambiaba de proyecto empezaba de cero. Un compilador escrito para un proyecto no servía para el siguiente. La idea de fondo fue entonces buscar un lenguaje común para sistemas embebidos, con la expectativa de amortizar herramientas y formación a lo largo de muchos programas.
Los documentos de requisitos
Lo que siguió es poco frecuente en la historia de los lenguajes: antes de diseñar nada, se escribió y se sometió a revisión pública una serie de documentos de requisitos, cada uno más preciso que el anterior. Los nombres siguen una broma interna que va endureciendo el material del “hombre” que representa el documento:
| Documento | Año aproximado | Qué aportó |
|---|---|---|
| Strawman | 1975 | Primer borrador de necesidades, deliberadamente frágil para invitar críticas |
| Woodenman | 1975-1976 | Refinamiento tras la primera ronda de comentarios |
| Tinman | 1976 | Primera lista concreta de capacidades exigidas al lenguaje |
| Ironman | 1977-1978 | Requisitos reorganizados y ya utilizables como base de un concurso |
| Steelman | 1978 | Versión final que sirvió de criterio para evaluar las propuestas |
En paralelo se evaluó si algún lenguaje existente podía adoptarse tal cual. Se revisaron candidatos como Pascal, PL/I y Algol 68. La conclusión fue que ninguno cumplía los requisitos completos, pero que Pascal era una base razonable sobre la cual construir. Ese detalle explica buena parte del aspecto de Ada: la estructura de declaraciones, el begin/end y la disciplina de tipos vienen de la familia Algol-Pascal.
La competencia de los colores
Con Ironman en la mano se convocó un concurso. Cuatro equipos presentaron propuestas y, para que la evaluación fuera ciega respecto del proveedor, cada una se identificó con un color:
| Color | Organización | Resultado |
|---|---|---|
| Verde | CII Honeywell Bull | Pasó a la segunda fase y ganó |
| Rojo | Intermetrics | Pasó a la segunda fase |
| Azul | SofTech | Eliminado en la primera fase |
| Amarillo | SRI International | Eliminado en la primera fase |
Verde y Rojo refinaron sus diseños durante una segunda ronda. El 2 de mayo de 1979 se anunció que la propuesta Verde, liderada por Jean Ichbiah, era la ganadora.
flowchart TD
A["1974: el DoD detecta<br/>cientos de lenguajes<br/>en sistemas embebidos"] --> B["1975-1978: serie de<br/>documentos de requisitos<br/>Strawman a Steelman"]
B --> C{"¿Sirve un lenguaje<br/>ya existente?"}
C -->|"Se evalúan Pascal,<br/>PL/I y Algol 68"| D["No cumplen Steelman,<br/>pero Pascal queda<br/>como base de partida"]
D --> E["Concurso de propuestas<br/>identificadas por color"]
E --> F["Verde<br/>CII Honeywell Bull"]
E --> G["Rojo<br/>Intermetrics"]
E --> H["Azul<br/>SofTech"]
E --> I["Amarillo<br/>SRI International"]
H --> J["Descartadas en<br/>la primera fase"]
I --> J
F --> K["Segunda fase<br/>de refinamiento"]
G --> K
K --> L["2 de mayo de 1979:<br/>gana Verde,<br/>equipo de Jean Ichbiah"]
L --> M["1983: estándar ANSI<br/>conocido como Ada 83"]
El nombre
El lenguaje se llamó Ada en honor a Augusta Ada Byron, condesa de Lovelace (1815-1852), colaboradora de Charles Babbage en el trabajo sobre la máquina analítica y autora de las notas donde aparece lo que se reconoce como el primer algoritmo pensado para ser ejecutado por una máquina. Conviene notar el detalle tipográfico: Ada no es un acrónimo, por eso se escribe como nombre propio y no en mayúsculas.
El primer estándar recibió además una identificación militar, MIL-STD-1815, cuyo número no es casual: 1815 es el año de nacimiento de Ada Lovelace.
Las revisiones del estándar
| Revisión | Aporte principal |
|---|---|
| Ada 83 | Primer estándar. Paquetes, tipos fuertes, excepciones, tareas, genéricos |
| Ada 95 | Orientación a objetos con tipos etiquetados, jerarquía de bibliotecas con unidades hijas, anexos especializados |
| Ada 2005 | Interfaces abstractas, tipos sincronizados, contenedores estándar, mejoras de tiempo real |
| Ada 2012 | Contratos ejecutables con precondiciones, poscondiciones e invariantes; expresiones condicionales y funciones de expresión; soporte multinúcleo |
| Ada 2022 | Aspectos ampliados, 'Image sobre objetos, literales definidos por el usuario, iteradores paralelos |
La revisión de 2012 es la que cambia más el estilo de escritura moderno, porque permite escribir la especificación de una función junto a su firma:
function Dividir (A, B : Integer) return Integer
with Pre => B /= 0,
Post => Dividir'Result * B <= A;
Ese with Pre no es un comentario: el compilador puede generar la verificación y, si está activada, el programa aborta con una excepción cuando alguien llama a Dividir con B = 0. Es la misma idea de “verificar antes de actuar” que aplica el kernel al validar los argumentos de una llamada al sistema, pero expresada en el lenguaje.
Ada frente a los lenguajes que ya conoces
Una tabla ayuda a situar el lenguaje respecto de C, que es el idioma histórico de los sistemas operativos, y de Elixir, que usaremos más adelante.
| Aspecto | C | Ada | Elixir |
|---|---|---|---|
| Verificación de tipos | Débil, con conversiones implícitas | Fuerte, conversiones explícitas obligatorias | Dinámica, en tiempo de ejecución |
| Rango de un valor | No forma parte del tipo | Puede formar parte del tipo, se verifica | No forma parte del tipo |
| Acceso fuera de un arreglo | Comportamiento indefinido | Excepción Constraint_Error | Excepción o error del proceso |
| Concurrencia | Biblioteca externa, hilos del sistema | Tareas y objetos protegidos en el lenguaje | Procesos ligeros del runtime BEAM |
| Manejo de errores | Códigos de retorno y errno | Excepciones con propagación definida | Fallar rápido y supervisar |
| Compilación | A código nativo | A código nativo | A bytecode sobre la máquina virtual BEAM |
| Separación interfaz/implementación | Convención con archivos .h | Obligatoria en el lenguaje: .ads y .adb | Módulos con funciones públicas y privadas |
| Uso típico | Kernels, drivers, runtimes | Aviónica, ferrocarril, defensa, espacio | Sistemas distribuidos, servicios de red |
La fila que más nos va a servir en este bloque del curso es la del rango. En C, un int que representa un porcentaje puede valer -7 y nadie se entera hasta que produce un resultado absurdo. En Ada puedes declarar subtype Porcentaje is Integer range 0 .. 100; y cualquier intento de asignar -7 detiene el programa en el punto exacto donde ocurrió el error, no tres módulos más adelante.
Instalación del compilador
Ada tiene varios compiladores, pero el que vamos a usar es GNAT, el front-end Ada de GCC. Al ser parte de GCC comparte generador de código, optimizador y enlazador con los compiladores de C y C++, lo que significa que el ejecutable que produce es un binario nativo idéntico en formato al que produce gcc sobre un .c.
Hay dos caminos de instalación y conviene entender la diferencia antes de elegir.
Camino 1: el paquete de la distribución
Instala GNAT desde el gestor de paquetes del sistema. Es el camino más corto y deja el compilador disponible para todo el sistema.
# Debian y Ubuntu
sudo apt update
sudo apt install gnat gprbuild
# Fedora
sudo dnf install gcc-gnat gprbuild
# Arch Linux
sudo pacman -S gcc-ada gprbuild
# openSUSE
sudo zypper install gcc-ada
Verifica que quedó instalado:
gnatmake --version
gnat --version
gcc --version
La salida debe mostrar el número de versión de GNAT. Si gnatmake responde pero gcc no reconoce archivos .adb, es porque la instalación quedó parcial: gnat provee el front-end, y sin él GCC no sabe qué hacer con código Ada.
Camino 2: Alire
Alire es el gestor de paquetes y proyectos del ecosistema Ada, comparable a cargo en Rust o mix en Elixir. Su ejecutable se llama alr. Además de manejar dependencias, puede instalar y administrar la propia cadena de herramientas, lo que evita depender de la versión que empaquete tu distribución.
Se instala descargando el binario desde el sitio del proyecto (alire.ada.dev) y dejándolo en el PATH:
# Ejemplo con una descarga manual del binario ya compilado
mkdir -p ~/.local/bin
# se descarga el archivo comprimido desde alire.ada.dev y se extrae
unzip alr-*-bin-x86_64-linux.zip -d ~/alire
cp ~/alire/bin/alr ~/.local/bin/
export PATH="$HOME/.local/bin:$PATH"
alr version
Con alr disponible, se selecciona la cadena de herramientas:
alr toolchain --select
Ese comando muestra un menú con las versiones disponibles de gnat_native y gprbuild, las descarga y las deja registradas para los proyectos que gestione Alire.
Cuál usar
| Criterio | Paquete de la distribución | Alire |
|---|---|---|
| Instalación | Un comando, requiere privilegios | Descarga de un binario, sin privilegios |
| Versión de GNAT | La que empaquete la distribución | Se elige explícitamente |
| Dependencias de terceros | A mano | Resueltas desde el índice de crates |
| Archivo de proyecto | .gpr escrito por ti | .gpr generado más alire.toml |
| Aislamiento entre proyectos | Ninguno, compilador global | Cada proyecto fija su cadena y dependencias |
Para los ejemplos de este capítulo cualquiera de los dos sirve. Vamos a mostrar los comandos en ambas formas.
Anatomía de un proyecto en Ada
Antes del primer programa hay que entender tres extensiones de archivo y una regla de nombres, porque GNAT no busca el código por su contenido sino por el nombre del archivo.
| Extensión | Nombre | Contenido |
|---|---|---|
.ads | Ada specification | La interfaz: qué ofrece una unidad, sin decir cómo |
.adb | Ada body | La implementación: el código que realiza lo declarado |
.gpr | GNAT project file | Descripción del proyecto: dónde están las fuentes, qué opciones usar, cuál es el programa principal |
La regla de nombres de GNAT es directa: el archivo se llama como la unidad, en minúsculas, y los puntos de una unidad anidada se reemplazan por guiones.
| Unidad Ada | Archivo de especificación | Archivo de cuerpo |
|---|---|---|
Hola (procedimiento principal) | no aplica | hola.adb |
Core | core.ads | core.adb |
Core.Conversion | core-conversion.ads | core-conversion.adb |
Ada.Text_IO | provisto por el runtime | provisto por el runtime |
Un procedimiento principal no necesita .ads: su interfaz es trivial, no lo usa nadie más y el enlazador solo necesita el cuerpo.
Un archivo .gpr mínimo, escrito a mano, se ve así:
project Ejemplos is
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Exec_Dir use "bin";
for Main use ("hola.adb");
package Compiler is
for Default_Switches ("Ada") use ("-gnatwa", "-gnata", "-g");
end Compiler;
end Ejemplos;
Cada línea dice algo concreto:
Source_Dirs: carpetas donde buscar.adsy.adb.Object_Dir: dónde dejar los.oy los.aliintermedios.Exec_Dir: dónde dejar los ejecutables.Main: qué archivos contienen programas principales; por cada uno se genera un ejecutable.Default_Switches ("Ada"): opciones que se pasan al compilador.-gnatwaactiva todas las advertencias,-gnatahabilita la verificación de precondiciones y poscondiciones,-gincluye información de depuración.
Se construye con:
gprbuild -P ejemplos.gpr
Primer programa: hola mundo
Crea el archivo hola.adb:
with Ada.Text_IO;
procedure Hola is
begin
Ada.Text_IO.Put_Line ("Hola, mundo");
end Hola;
Compílalo y ejecútalo:
gnatmake hola.adb
./hola
Salida:
Hola, mundo
Con Alire el flujo es distinto porque se parte de un proyecto:
alr init --bin hola
cd hola
# el código va en src/hola.adb
alr build
alr run
Explicación línea a línea
with Ada.Text_IO;
Esta es la cláusula de contexto. Le dice al compilador que esta unidad depende de la unidad de biblioteca Ada.Text_IO. Dos consecuencias prácticas: primero, los nombres declarados en ese paquete pasan a ser visibles usando su nombre completo; segundo, el sistema de compilación sabe que si Ada.Text_IO cambia, esta unidad debe recompilarse.
No es un #include de C. #include copia texto de un archivo dentro de otro y ocurre antes de que el compilador vea el código. with declara una dependencia entre unidades ya compiladas y el compilador la usa para verificar coherencia. Si un cuerpo cambia sin cambiar su especificación, quien depende de él no necesita recompilarse.
procedure Hola is
Declara un procedimiento llamado Hola. Un procedimiento es un subprograma que no devuelve valor; el que sí devuelve se declara con function. Como este procedimiento está en el nivel superior de un archivo y no dentro de un paquete, es una unidad de biblioteca, y cualquier unidad de biblioteca sin parámetros puede servir como punto de entrada del programa.
El is abre la parte declarativa: entre is y begin van las declaraciones locales (variables, constantes, tipos, subprogramas anidados). En este programa no hay ninguna, así que la sección está vacía.
Aquí aparece la primera diferencia visible con C. En C, main es un nombre reservado por convención de la biblioteca estándar. En Ada el punto de entrada se llama como quieras; lo que lo convierte en principal es que el enlazador lo elija, cosa que ocurre al pasarlo a gnatmake o al declararlo en for Main use del .gpr.
begin
Cierra la parte declarativa y abre la parte ejecutable. Todo lo que va después son sentencias que se ejecutan en orden.
Ada.Text_IO.Put_Line ("Hola, mundo");
Llama al procedimiento Put_Line del paquete Ada.Text_IO pasando una cadena literal. Put_Line escribe el texto en la salida estándar y agrega un salto de línea. Su pariente Put escribe sin salto, y New_Line emite solo el salto.
El nombre está completamente calificado: paquete, punto, subprograma. Esto es verboso a propósito; más abajo veremos cómo acortarlo con use y por qué a veces conviene no hacerlo.
end Hola;
Cierra el procedimiento. El nombre después de end es opcional en algunos contextos y obligatorio en otros, pero la práctica habitual es siempre repetirlo: en un archivo de trescientas líneas con bloques anidados, end Hola; deja claro qué se está cerrando sin necesidad de contar la indentación.
Qué ocurre cuando compilas
gnatmake no es un compilador: es un conductor que decide qué hay que recompilar y en qué orden, y luego invoca tres herramientas distintas.
sequenceDiagram
participant U as Usuario
participant M as gnatmake
participant C as gcc con front-end Ada
participant B as gnatbind
participant L as gnatlink
participant FS as Sistema de archivos
U->>M: gnatmake hola.adb
M->>FS: leer hola.adb y sus dependencias
M->>M: comparar marcas de tiempo<br/>de .adb, .ali y .o
M->>C: compilar unidades desactualizadas
C->>FS: escribir hola.o
C->>FS: escribir hola.ali con la<br/>información de dependencias
M->>B: gnatbind hola.ali
B->>FS: verificar consistencia de<br/>todas las unidades
B->>FS: generar b~hola.adb con<br/>el orden de elaboración
M->>L: gnatlink hola.ali
L->>FS: enlazar objetos y runtime
L->>FS: escribir el ejecutable hola
M-->>U: binario listo
Las tres etapas hacen cosas diferentes y vale la pena separarlas:
- Compilar (
gcc -c hola.adb). Traduce cada unidad a código objeto. Además del.o, produce un archivo.ali(Ada Library Information) que registra de qué unidades depende, con qué versión y con qué opciones se compiló. - Ligar (
gnatbind hola.ali). Lee los.alide todas las unidades involucradas y comprueba que todas se compilaron contra las mismas versiones de sus dependencias. Si compilaste un paquete, cambiaste su especificación y olvidaste recompilar a quien lo usa, esta etapa lo detecta y falla. Luego genera un archivo fuente auxiliar, típicamenteb~hola.adb, que contiene el orden en que deben elaborarse las unidades, es decir, el orden en que se inicializan sus variables y se ejecutan sus bloques de arranque antes de llamar al programa principal. - Enlazar (
gnatlink hola.ali). Invoca al enlazador del sistema con los objetos, el archivo de ligado y la biblioteca de runtime de Ada, y produce el ejecutable.
Puedes ejecutar las tres a mano para verlas:
gcc -c hola.adb
gnatbind hola.ali
gnatlink hola.ali
./hola
Y observar los archivos generados:
ls -1
# b~hola.adb
# b~hola.ads
# b~hola.ali
# b~hola.o
# hola
# hola.adb
# hola.ali
# hola.o
Esa etapa de ligado es la que no existe en C y explica una diferencia de comportamiento importante: en Ada, la verificación de que todo el programa es coherente ocurre antes de enlazar, no en tiempo de ejecución ni por accidente. Es el mismo principio que aplica el cargador del sistema operativo cuando resuelve símbolos de bibliotecas dinámicas, pero adelantado a la construcción.
with y use
Escribir Ada.Text_IO.Put_Line cada vez es tedioso. La cláusula use hace visibles los nombres de un paquete sin calificar:
with Ada.Text_IO;
procedure Hola is
use Ada.Text_IO;
begin
Put_Line ("Hola, mundo");
Put ("Sin salto de línea");
New_Line;
Put_Line ("Otra línea");
end Hola;
La distinción entre las dos cláusulas es la siguiente:
| Cláusula | Qué hace | Dónde puede ir |
|---|---|---|
with P; | Declara la dependencia y permite escribir P.Algo | Solo antes de una unidad de biblioteca |
use P; | Hace visibles los nombres de P sin prefijo | En cláusula de contexto o dentro de una parte declarativa |
with es obligatorio; use es opcional y siempre requiere un with previo. Ponerlo dentro del procedimiento, como en el ejemplo, limita su efecto a ese ámbito, lo que reduce las posibilidades de que dos paquetes distintos exporten el mismo nombre y el compilador no pueda decidir cuál usaste. Cuando eso ocurre el error es de ambigüedad y se resuelve calificando el nombre en el punto conflictivo.
Existe además use type, que hace visibles solo los operadores de un tipo:
with Ada.Numerics.Elementary_Functions;
procedure Ejemplo is
use type Standard.Float;
begin
null;
end Ejemplo;
Es la forma más acotada: permite escribir A + B sin importar todo el paquete.
Segundo programa: raíz cuadrada
El segundo ejemplo introduce tres cosas: subprogramas con parámetros, la biblioteca matemática, y el atributo 'Image para convertir valores a texto.
Versión con un procedimiento anidado
Archivo raiz.adb:
with Ada.Text_IO;
with Ada.Numerics.Elementary_Functions;
procedure Raiz is
procedure Mostrar_Raiz (Numero : Float) is
use Ada.Text_IO;
use Ada.Numerics.Elementary_Functions;
begin
Put_Line (Float'Image (Sqrt (Numero)));
end Mostrar_Raiz;
begin
Mostrar_Raiz (9.0);
Mostrar_Raiz (2.0);
Mostrar_Raiz (144.0);
end Raiz;
Compilación y salida:
gnatmake raiz.adb
./raiz
3.00000E+00
1.41421E+00
1.20000E+02
Explicación línea a línea
with Ada.Numerics.Elementary_Functions;
Este paquete forma parte del anexo numérico del estándar y ofrece las funciones matemáticas elementales para el tipo Float: Sqrt, Sin, Cos, Tan, Log, Exp, Arctan y sus variantes. Es en realidad una instancia predefinida del paquete genérico Ada.Numerics.Generic_Elementary_Functions, que puede instanciarse para cualquier tipo de punto flotante que definas:
with Ada.Numerics.Generic_Elementary_Functions;
procedure Precision_Alta is
type Real is digits 15;
package Real_Funciones is
new Ada.Numerics.Generic_Elementary_Functions (Real);
use Real_Funciones;
X : Real := Sqrt (2.0);
begin
null;
end Precision_Alta;
Ese type Real is digits 15; declara un tipo de punto flotante con al menos quince dígitos decimales significativos. Ada no te obliga a saber si eso corresponde a un double de la máquina: tú declaras la precisión que el problema necesita y el compilador elige la representación, o rechaza el programa si el hardware no puede darla.
procedure Mostrar_Raiz (Numero : Float) is
Un procedimiento anidado dentro de otro. Se declara en la parte declarativa de Raiz, entre el is y el begin del procedimiento externo. Solo es visible ahí dentro.
El parámetro se escribe Nombre : Tipo. Cuando no se indica el modo, es in, que significa solo lectura: dentro del subprograma, Numero es una constante. Los tres modos posibles son:
| Modo | Significado | Uso típico |
|---|---|---|
in | Solo lectura, es el modo por defecto | Datos de entrada |
out | Solo escritura, el valor inicial no está definido | Resultados adicionales |
in out | Lectura y escritura, se recibe y se modifica | Estructuras que se actualizan |
Esta declaración explícita del modo es una diferencia notable con C, donde para lograr lo mismo hay que pasar punteros y confiar en que el llamado respete la intención, o marcarlos const y confiar en que nadie haga un cast.
use Ada.Text_IO; y use Ada.Numerics.Elementary_Functions;
Ambas dentro de la parte declarativa del procedimiento anidado. Su efecto termina en end Mostrar_Raiz;. Gracias a ellas se puede escribir Put_Line y Sqrt sin prefijo.
Put_Line (Float'Image (Sqrt (Numero)));
Aquí hay tres operaciones anidadas que conviene leer de adentro hacia afuera:
Sqrt (Numero)calcula la raíz cuadrada y devuelve unFloat.Float'Image (...)aplica un atributo al tipoFloat. Los atributos se escriben con comilla simple y son consultas o funciones asociadas a un tipo o a un objeto.'Imageconvierte un valor a su representación textual estándar y devuelve unString.Put_Line (...)escribe eseString.
El resultado 3.00000E+00 merece dos comentarios. El primero es el espacio inicial: 'Image reserva la primera posición para el signo, y en los valores no negativos queda un espacio en blanco. El segundo es la notación exponencial: ese es el formato por defecto de 'Image para tipos de punto flotante. Si quieres una tabla alineada con decimales fijos, hay que usar Ada.Float_Text_IO, que veremos en el tercer programa.
En Ada 2022 se puede aplicar 'Image directamente sobre el objeto, lo que acorta la escritura:
Put_Line (Sqrt (Numero)'Image);
Para que el compilador acepte esa forma hay que pedirle esa versión del lenguaje:
gnatmake -gnat2022 raiz.adb
o declararlo en el archivo de proyecto:
package Compiler is
for Default_Switches ("Ada") use ("-gnat2022");
end Compiler;
Si escribes X'Image sin habilitar Ada 2022 y tu compilador aplica estrictamente Ada 2012, el error indicará que el prefijo del atributo debe ser un tipo. La forma Float'Image (X) funciona en todas las versiones.
Qué pasa con un número negativo
Sqrt no está definida para argumentos negativos. Si escribes Mostrar_Raiz (-4.0); el programa compila sin problema, pero al ejecutarse aborta con una excepción:
raised ADA.NUMERICS.ARGUMENT_ERROR : ...
Este es el comportamiento base de Ada frente a un error: no devolver un valor especial que el llamador podría ignorar, sino interrumpir el flujo normal. Para manejarlo se agrega un manejador de excepciones:
with Ada.Text_IO;
with Ada.Numerics;
with Ada.Numerics.Elementary_Functions;
procedure Raiz_Segura is
procedure Mostrar_Raiz (Numero : Float) is
use Ada.Text_IO;
use Ada.Numerics.Elementary_Functions;
begin
Put_Line (Float'Image (Sqrt (Numero)));
exception
when Ada.Numerics.Argument_Error =>
Put_Line ("Sin raíz real para " & Float'Image (Numero));
end Mostrar_Raiz;
begin
Mostrar_Raiz (9.0);
Mostrar_Raiz (-4.0);
Mostrar_Raiz (2.0);
end Raiz_Segura;
Salida:
3.00000E+00
Sin raíz real para -4.00000E+00
1.41421E+00
La sección exception va antes del end del bloque y cada when captura un tipo de excepción. Si ninguna cláusula coincide, la excepción sigue propagándose hacia el llamador, y si llega hasta el nivel del programa principal sin ser capturada, el runtime aborta imprimiendo el nombre de la excepción.
flowchart TD
A["Llamada a Mostrar_Raiz"] --> B["Sqrt evalúa el argumento"]
B --> C{"¿El argumento<br/>es negativo?"}
C -->|No| D["Devuelve el valor<br/>y se imprime"]
C -->|Sí| E["Se eleva<br/>Argument_Error"]
E --> F{"¿Hay un manejador<br/>en este bloque?"}
F -->|Sí| G["Se ejecuta el when<br/>y el bloque termina normalmente"]
F -->|No| H["Se propaga al<br/>llamador inmediato"]
H --> I{"¿Queda algún<br/>llamador arriba?"}
I -->|Sí| F
I -->|No| J["El runtime imprime la<br/>excepción y aborta el proceso"]
D --> K["Continúa el programa"]
G --> K
El paralelo con el sistema operativo es directo. Cuando un proceso hace una división entera por cero, el hardware genera una excepción, el kernel la convierte en una señal SIGFPE y, si el proceso no instaló un manejador, lo termina. La estructura es la misma: se eleva una condición, se busca un manejador subiendo por una cadena, y si no hay ninguno el nivel superior aborta. Ada implementa ese patrón dentro del programa; el kernel lo implementa entre el hardware y el proceso.
Versión con paquete propio
El procedimiento anidado sirve para un ejemplo, pero no se puede reutilizar desde otro programa. La forma habitual de organizar código en Ada es el paquete, que se escribe en dos archivos: la especificación y el cuerpo.
Archivo core.ads (especificación):
package Core is
-- Imprime la raíz cuadrada de Numero en la salida estándar.
-- Si Numero es negativo imprime un mensaje en lugar de abortar.
procedure Mostrar_Raiz (Numero : Float);
-- Devuelve la raíz cuadrada, o 0.0 si el argumento es negativo.
function Raiz_O_Cero (Numero : Float) return Float;
end Core;
Archivo core.adb (cuerpo):
with Ada.Text_IO;
with Ada.Numerics;
with Ada.Numerics.Elementary_Functions;
package body Core is
procedure Mostrar_Raiz (Numero : Float) is
use Ada.Text_IO;
begin
Put_Line (Float'Image (Raiz_O_Cero (Numero)));
end Mostrar_Raiz;
function Raiz_O_Cero (Numero : Float) return Float is
use Ada.Numerics.Elementary_Functions;
begin
if Numero < 0.0 then
return 0.0;
else
return Sqrt (Numero);
end if;
end Raiz_O_Cero;
end Core;
Archivo raiz.adb (programa principal):
with Core;
procedure Raiz is
begin
Core.Mostrar_Raiz (9.0);
Core.Mostrar_Raiz (-4.0);
Core.Mostrar_Raiz (2.0);
end Raiz;
Compilación:
gnatmake raiz.adb
./raiz
3.00000E+00
0.00000E+00
1.41421E+00
Nota que raiz.adb solo hace with Core;. No menciona Ada.Text_IO ni la biblioteca matemática, porque no las usa: las usa el cuerpo de Core, y las cláusulas de contexto del cuerpo no se propagan hacia quien usa el paquete. Ese es el punto central de la separación entre .ads y .adb.
flowchart LR
subgraph Principal["raiz.adb"]
R["procedure Raiz"]
end
subgraph Interfaz["core.ads"]
S["Mostrar_Raiz<br/>Raiz_O_Cero"]
end
subgraph Implementacion["core.adb"]
B["package body Core"]
end
subgraph Runtime["Biblioteca estándar"]
T["Ada.Text_IO"]
N["Ada.Numerics.<br/>Elementary_Functions"]
end
R -->|"with Core"| S
B -->|"implementa"| S
B -->|"with"| T
B -->|"with"| N
R -.->|"no depende<br/>de estas"| T
Consecuencias prácticas de esa separación:
| Cambio realizado | Qué hay que recompilar |
|---|---|
Se modifica solo core.adb | El cuerpo de Core, y se vuelve a enlazar |
Se modifica core.ads | El cuerpo de Core y todo lo que hace with Core |
Se agrega un with en core.adb | Solo el cuerpo de Core |
Se agrega un subprograma a core.ads | Todo lo que hace with Core, aunque no lo use |
En un proyecto grande esta regla es la diferencia entre una compilación de segundos y una de minutos, y explica por qué en Ada se procura que las especificaciones sean estables y pequeñas.
Tercer programa: conversor de temperatura
El tercer ejemplo produce una tabla de conversión de Fahrenheit a Celsius. Es un programa clásico, y sirve para mostrar bucles, salida formateada y —sobre todo— dónde muerde la aritmética entera.
Versión con enteros
Archivo temperatura.adb:
with Ada.Text_IO;
procedure Temperatura is
use Ada.Text_IO;
Fahrenheit, Celsius : Integer;
Inferior, Superior, Paso : Integer;
begin
Inferior := 0;
Superior := 300;
Paso := 20;
Put_Line (" F" & ASCII.HT & " C");
Fahrenheit := Inferior;
while Fahrenheit <= Superior loop
Celsius := 5 * (Fahrenheit - 32) / 9;
Put_Line (Integer'Image (Fahrenheit) & ASCII.HT &
Integer'Image (Celsius));
Fahrenheit := Fahrenheit + Paso;
end loop;
end Temperatura;
Salida:
F C
0 -17
20 -6
40 4
60 15
80 26
100 37
120 48
140 60
160 71
180 82
200 93
220 104
240 115
260 126
280 137
300 148
Explicación línea a línea
use Ada.Text_IO; en la parte declarativa
Aquí el use está fuera de cualquier subprograma anidado, en la parte declarativa del procedimiento principal, así que vale para todo el cuerpo.
Fahrenheit, Celsius : Integer;
Declaración de dos variables del mismo tipo en una línea. Integer es el tipo entero predefinido; su rango exacto depende de la implementación, pero puedes consultarlo con atributos:
Put_Line (Integer'Image (Integer'First));
Put_Line (Integer'Image (Integer'Last));
En una implementación de 32 bits eso imprime -2147483648 y 2147483647.
Estas variables no tienen valor inicial. En Ada leer una variable no inicializada no es un error del compilador pero sí un error del programa; una forma de evitarlo es inicializar en la propia declaración:
Fahrenheit : Integer := 0;
Inferior := 0;
El operador de asignación es :=, no =. En Ada, = es el operador de comparación de igualdad, y /= el de desigualdad. Esta separación elimina de raíz la confusión clásica de C entre = y ==: escribir if Fahrenheit = 32 then compara, y no hay forma de asignar por accidente dentro de una condición porque la asignación es una sentencia, no una expresión.
Put_Line (" F" & ASCII.HT & " C");
El operador & concatena. Puede unir dos String, un String con un Character o dos Character.
ASCII.HT es el carácter de tabulación horizontal. ASCII es un paquete anidado dentro de Standard, disponible sin ningún with, que declara nombres para los caracteres de control. Se mantiene por compatibilidad desde Ada 83; la forma moderna equivalente es:
with Ada.Characters.Latin_1;
-- ...
Put_Line (" F" & Ada.Characters.Latin_1.HT & " C");
while Fahrenheit <= Superior loop ... end loop;
El bucle while evalúa la condición antes de cada iteración. Nota que no hay paréntesis obligatorios alrededor de la condición y que el bloque se cierra con end loop;.
Ada tiene tres formas de bucle:
| Forma | Sintaxis | Cuándo se usa |
|---|---|---|
| Bucle simple | loop ... end loop; | Repetición infinita, se sale con exit when |
Bucle while | while Condición loop ... end loop; | La cantidad de vueltas depende de una condición |
Bucle for | for I in 0 .. 15 loop ... end loop; | Se recorre un rango discreto conocido |
Un detalle del for de Ada que sorprende a quien viene de C: no existe un incremento configurable. for I in 1 .. 10 loop avanza de uno en uno, y for I in reverse 1 .. 10 loop retrocede de uno en uno. La variable del bucle se declara sola, es constante dentro del cuerpo y desaparece al salir. Para avanzar de veinte en veinte hay que calcularlo:
for I in 0 .. 15 loop
Fahrenheit := I * 20;
-- ...
end loop;
Celsius := 5 * (Fahrenheit - 32) / 9;
Esta línea concentra el punto didáctico del programa. Al ser todos los operandos de tipo Integer, la división es entera: descarta la parte fraccionaria truncando hacia cero. Por eso 0 °F aparece como -17 y no como -17,8.
El orden importa. * y / tienen la misma precedencia y se evalúan de izquierda a derecha, de modo que primero se multiplica por 5 y después se divide por 9. Si se escribiera (Fahrenheit - 32) / 9 * 5, la división truncaría antes de multiplicar y el error se amplificaría por cinco. Es la misma trampa que aparece al calcular porcentajes o promedios con enteros en cualquier lenguaje, y en un sistema de control acumulada sobre miles de iteraciones deja de ser un detalle.
Integer'Image (Fahrenheit)
Igual que antes, convierte el valor a texto. Para enteros no hay notación exponencial, pero sí queda el espacio reservado para el signo: los valores positivos salen precedidos de un espacio y los negativos del -. Eso explica la alineación irregular de la salida.
Versión con punto flotante y salida formateada
Archivo temperatura_float.adb:
with Ada.Text_IO;
with Ada.Float_Text_IO;
procedure Temperatura_Float is
use Ada.Text_IO;
use Ada.Float_Text_IO;
Fahrenheit, Celsius : Float;
Inferior, Superior, Paso : Float;
begin
Inferior := 0.0;
Superior := 300.0;
Paso := 20.0;
Put_Line (" F C");
Fahrenheit := Inferior;
while Fahrenheit <= Superior loop
Celsius := 5.0 * (Fahrenheit - 32.0) / 9.0;
Put (Item => Fahrenheit, Fore => 5, Aft => 1, Exp => 0);
Put (Item => Celsius, Fore => 5, Aft => 1, Exp => 0);
New_Line;
Fahrenheit := Fahrenheit + Paso;
end loop;
end Temperatura_Float;
Salida:
F C
0.0 -17.8
20.0 -6.7
40.0 4.4
60.0 15.6
80.0 26.7
100.0 37.8
120.0 48.9
140.0 60.0
160.0 71.1
180.0 82.2
200.0 93.3
220.0 104.4
240.0 115.6
260.0 126.7
280.0 137.8
300.0 148.9
Los cambios respecto de la versión entera:
with Ada.Float_Text_IO;
Este paquete es una instancia predefinida del genérico Ada.Text_IO.Float_IO para el tipo Float. Aporta procedimientos Get y Put que trabajan con valores de punto flotante y permiten controlar el formato.
Inferior := 0.0; en lugar de Inferior := 0;
Aquí aparece la regla más característica del lenguaje. En Ada, 0 es un literal entero universal y 0.0 es un literal real universal; asignar 0 a una variable Float es un error de compilación, no una conversión silenciosa. El mensaje es del estilo expected type "Standard.Float", found type universal integer.
Lo mismo vale para mezclar variables. Esto no compila:
declare
N : Integer := 9;
X : Float := 2.0;
Y : Float;
begin
Y := X * N; -- error: no se puede multiplicar Float por Integer
end;
La forma correcta es convertir explícitamente:
Y := X * Float (N);
La conversión se escribe como si el tipo fuera una función. Ese es el tipado fuerte en la práctica: cuesta un par de caracteres extra y, a cambio, la pregunta “¿en qué unidad está este número?” nunca se responde por accidente.
Put (Item => Fahrenheit, Fore => 5, Aft => 1, Exp => 0);
Esta llamada usa asociación con nombre: cada argumento se identifica por el nombre de su parámetro en vez de por su posición. Es legal en cualquier llamada de Ada y es especialmente útil cuando hay varios parámetros del mismo tipo, porque hace imposible confundir el orden.
Los parámetros de este Put significan:
| Parámetro | Significado | Efecto de subirlo |
|---|---|---|
Item | El valor a imprimir | — |
Fore | Ancho mínimo de la parte entera, incluido el signo | Más espacios a la izquierda, columnas alineadas |
Aft | Cantidad de dígitos tras el punto decimal | Más decimales visibles |
Exp | Ancho del campo del exponente; 0 desactiva la notación científica | Con 0 se imprime en notación posicional |
Con Exp => 0 desaparece la notación 3.00000E+00 que veíamos con 'Image, y con Fore fijo las columnas quedan alineadas sin necesidad de tabulaciones.
Versión con tipos propios
El programa anterior funciona, pero tiene un problema conceptual: Fahrenheit y Celsius son ambos Float, así que nada impide sumarlos entre sí o asignar uno al otro. Ada permite crear tipos distintos a partir del mismo tipo base para que el compilador impida esa mezcla.
Archivo temperatura_tipada.adb:
with Ada.Text_IO;
procedure Temperatura_Tipada is
type Grados_F is new Float;
type Grados_C is new Float;
package F_IO is new Ada.Text_IO.Float_IO (Grados_F);
package C_IO is new Ada.Text_IO.Float_IO (Grados_C);
function A_Celsius (F : Grados_F) return Grados_C is
(Grados_C ((Float (F) - 32.0) * 5.0 / 9.0));
use Ada.Text_IO;
Inferior : constant Grados_F := 0.0;
Superior : constant Grados_F := 300.0;
Paso : constant Grados_F := 20.0;
F : Grados_F := Inferior;
begin
Put_Line (" F C");
while F <= Superior loop
F_IO.Put (Item => F, Fore => 5, Aft => 1, Exp => 0);
C_IO.Put (Item => A_Celsius (F), Fore => 5, Aft => 1, Exp => 0);
New_Line;
F := F + Paso;
end loop;
end Temperatura_Tipada;
Las novedades, una por una:
type Grados_F is new Float;
Declara un tipo derivado. Grados_F tiene la misma representación interna que Float y hereda sus operadores, pero es un tipo diferente: Grados_F y Grados_C no son compatibles entre sí ni con Float. Si intentas F := A_Celsius (F); el compilador rechaza el programa, porque estarías guardando grados Celsius en una variable de grados Fahrenheit. Ese es exactamente el tipo de error que provoca fallos en sistemas reales cuando dos subsistemas usan unidades distintas.
Conviene distinguirlo de un subtipo:
subtype Temperatura_Valida is Float range -273.15 .. 10_000.0;
Un subtipo no crea un tipo nuevo: es el mismo tipo con una restricción de rango. Se mezcla libremente con su tipo base, y lo que aporta es una verificación en tiempo de ejecución: asignar un valor fuera del rango eleva Constraint_Error. Nota el guion bajo en 10_000.0; Ada permite separadores en literales numéricos para hacerlos legibles y el compilador los ignora.
| Construcción | ¿Tipo nuevo? | ¿Se mezcla con el base? | Verificación |
|---|---|---|---|
type T is new Float; | Sí | No, requiere conversión explícita | En compilación |
subtype S is Float range A .. B; | No | Sí | En ejecución, Constraint_Error |
type T is digits 15; | Sí, independiente | No | En compilación |
package F_IO is new Ada.Text_IO.Float_IO (Grados_F);
Como Grados_F es un tipo nuevo, Ada.Float_Text_IO no sirve: ese paquete solo sabe imprimir Float. Hay que instanciar el genérico Ada.Text_IO.Float_IO para el tipo propio. Esta línea crea un paquete nuevo, con su propio Put y su propio Get, especializado en Grados_F.
Los genéricos son el mecanismo de Ada para el código parametrizado por tipos, equivalente en propósito a las plantillas de C++ o a los genéricos de Rust. A diferencia de esos, la instancia se declara explícitamente y recibe un nombre, lo que hace visible en el código dónde se generó cada versión.
function A_Celsius (F : Grados_F) return Grados_C is (...)
Una función de expresión, introducida en Ada 2012. Cuando el cuerpo es una sola expresión, se puede escribir entre paréntesis en lugar de un bloque is begin return ... end. La forma larga equivalente sería:
function A_Celsius (F : Grados_F) return Grados_C is
begin
return Grados_C ((Float (F) - 32.0) * 5.0 / 9.0);
end A_Celsius;
Dentro se hacen dos conversiones: Float (F) baja de Grados_F al tipo base para poder operar con literales, y Grados_C (...) sube el resultado al tipo de salida. Cada conversión es visible en el código, que es precisamente el objetivo.
Inferior : constant Grados_F := 0.0;
La palabra constant declara un valor que no puede modificarse. Cualquier asignación posterior es un error de compilación. Es el equivalente a const en C, pero sin las excepciones que permiten evadirlo.
stateDiagram-v2
[*] --> Inicializacion: F := Inferior
Inicializacion --> Evaluacion
Evaluacion --> Conversion: F <= Superior
Evaluacion --> Fin: F > Superior
Conversion --> Impresion: A_Celsius aplica<br/>Grados_F a Grados_C
Impresion --> Incremento: F_IO.Put y C_IO.Put
Incremento --> Evaluacion: F := F + Paso
Fin --> [*]
note right of Conversion
El compilador verifica aquí
que no se mezclen unidades
end note
Leer datos desde el teclado
Los tres programas anteriores tienen sus valores fijos en el código. Para cerrar el capítulo agregamos entrada del usuario, que es donde aparecen los errores de conversión y donde el manejo de excepciones deja de ser teórico.
Archivo raiz_interactiva.adb:
with Ada.Text_IO;
with Ada.Numerics;
with Ada.Numerics.Elementary_Functions;
procedure Raiz_Interactiva is
use Ada.Text_IO;
use Ada.Numerics.Elementary_Functions;
Linea : String (1 .. 100);
Largo : Natural;
Numero : Float;
begin
loop
Put ("Número (o una línea vacía para salir): ");
Get_Line (Linea, Largo);
exit when Largo = 0;
begin
Numero := Float'Value (Linea (1 .. Largo));
Put_Line ("Raíz: " & Float'Image (Sqrt (Numero)));
exception
when Constraint_Error =>
Put_Line ("Eso no es un número válido.");
when Ada.Numerics.Argument_Error =>
Put_Line ("No hay raíz real de un número negativo.");
end;
end loop;
Put_Line ("Fin.");
end Raiz_Interactiva;
Las piezas nuevas:
Linea : String (1 .. 100);declara una cadena de longitud fija. En Ada unStringes un arreglo deCharactercuyo largo forma parte del objeto y no cambia; no existe el terminador nulo de C. Los detalles de arreglos y cadenas de longitud variable son el tema del capítulo siguiente.Get_Line (Linea, Largo);lee una línea completa de la entrada estándar. Copia los caracteres enLineay deja enLargocuántos leyó realmente. Por eso después se usaLinea (1 .. Largo), que es un corte del arreglo, no la cadena completa con relleno.Naturales un subtipo predefinido deIntegercon rango0 .. Integer'Last. Su parientePositiveempieza en 1. Usarlos comunica la intención y hace que un valor negativo sea imposible.exit when Largo = 0;sale del bucle cuando la condición se cumple. Es la forma de terminar unloopsimple.Float'Value (...)es el inverso de'Image: toma unStringy devuelve el valor correspondiente. Si el texto no representa un número válido, elevaConstraint_Error.- El bloque
begin ... exception ... endinterno acota el manejo de errores a esas dos líneas. Sin él, una excepción saldría del bucle y terminaría el programa; con él, se informa el problema y se vuelve a pedir un número.
Paquetes estándar de referencia
Estos son los paquetes que aparecen con más frecuencia al empezar. Todos se activan con una cláusula with.
| Paquete | Para qué sirve | Elementos frecuentes |
|---|---|---|
Ada.Text_IO | Entrada y salida de texto | Put, Put_Line, Get_Line, New_Line |
Ada.Integer_Text_IO | Entrada y salida de Integer | Put con Width y Base, Get |
Ada.Float_Text_IO | Entrada y salida de Float | Put con Fore, Aft, Exp |
Ada.Text_IO.Float_IO | Genérico para instanciar con tipos propios | Se instancia con new |
Ada.Numerics | Constantes y excepciones numéricas | Pi, e, Argument_Error |
Ada.Numerics.Elementary_Functions | Matemática para Float | Sqrt, Sin, Cos, Log, Exp |
Ada.Numerics.Generic_Elementary_Functions | Genérico de lo anterior | Se instancia con tu tipo real |
Ada.Characters.Latin_1 | Nombres de caracteres | HT, LF, CR, NUL |
Ada.Strings.Fixed | Operaciones sobre cadenas de largo fijo | Trim, Index, Move |
Ada.Strings.Unbounded | Cadenas de largo variable | Unbounded_String, To_String |
Ada.Command_Line | Argumentos y código de salida | Argument_Count, Argument, Set_Exit_Status |
Ada.Exceptions | Información sobre excepciones | Exception_Name, Exception_Message |
Ada.Calendar | Fecha y hora | Clock, Year, Month, Seconds |
Errores comunes y cómo resolverlos
| Error o síntoma | Causa | Solución |
|---|---|---|
file "hola.ali" not found o el compilador no encuentra la unidad | El nombre del archivo no coincide con el de la unidad | Renombrar el archivo a la unidad en minúsculas, con guiones en lugar de puntos: Core.Utils va en core-utils.adb |
expected type "Standard.Float", found type universal integer | Se asignó un literal entero a una variable real | Escribir el literal con parte decimal: 0.0 en lugar de 0 |
invalid operand types for operator "*" | Se mezclaron Integer y Float en una expresión | Convertir explícitamente: X * Float (N) |
| El resultado de una división sale truncado | Ambos operandos son enteros, la división es entera | Usar tipos reales o convertir antes de dividir |
"Put_Line" is undefined | Falta with Ada.Text_IO; o falta el prefijo del paquete | Agregar la cláusula with y usar Ada.Text_IO.Put_Line o un use |
ambiguous expression (cannot resolve "Put") | Dos paquetes con use exportan el mismo nombre | Calificar la llamada: Ada.Float_Text_IO.Put (...) |
prefix of "Image" attribute must be a type | Se usó X'Image sin habilitar Ada 2022 | Escribir Float'Image (X) o compilar con -gnat2022 |
raised CONSTRAINT_ERROR : ... range check failed | Un valor salió del rango del subtipo o del índice | Revisar el rango declarado; validar la entrada antes de asignar |
raised CONSTRAINT_ERROR : ... access check failed | Se usó un puntero sin inicializar | Inicializar el acceso o verificar contra null antes de desreferenciar |
raised ADA.NUMERICS.ARGUMENT_ERROR | Se llamó a Sqrt o Log con un argumento fuera de dominio | Validar el argumento o capturar la excepción |
raised STORAGE_ERROR : stack overflow | Recursión sin caso base o un objeto local demasiado grande | Corregir la recursión o mover el objeto grande al montón |
elaboration circularity detected en la etapa de bind | Dos unidades dependen mutuamente en su inicialización | Romper el ciclo moviendo la dependencia al cuerpo o usando pragma Elaborate_Body |
| Los acentos salen como caracteres extraños | Discordancia entre la codificación del archivo fuente y la que asume el compilador | Compilar con -gnatW8 si el archivo está en UTF-8, o limitar los literales a caracteres ASCII |
gnatmake: "hola.adb" compilation error sin más detalle | El error real está más arriba en la salida | Leer la salida completa desde el principio; el primer error suele explicar los siguientes |
El binario no se regenera tras editar un .ads | Se compiló solo el cuerpo a mano con gcc -c | Usar gnatmake o gprbuild, que recalculan las dependencias |
Ejercicios propuestos
1. Verificar la instalación en detalle. Instala GNAT por el camino de la distribución y también Alire. Compila el mismo hola.adb con ambos y compara los binarios con ls -l y file. Ejecuta ldd sobre cada uno y anota qué bibliotecas dinámicas requiere cada versión.
2. Desarmar la compilación. Ejecuta gcc -c, gnatbind y gnatlink por separado sobre hola.adb. Abre hola.ali con un editor de texto y describe qué información contiene. Después abre b~hola.adb e identifica dónde se llama al procedimiento principal.
3. Provocar un fallo de ligado. Crea el paquete Core de la sección de raíz cuadrada y el programa principal. Compila todo y ejecuta. Luego modifica solo core.ads agregando un parámetro a Mostrar_Raiz, recompila únicamente core.adb con gcc -c core.adb, e intenta enlazar a mano. Documenta el mensaje de error y explica en qué etapa apareció.
4. Precisión y truncamiento. Escribe un programa que calcule el promedio de los enteros 1, 2 y 4 de dos formas: con aritmética entera y con conversión a Float antes de dividir. Imprime ambos resultados. Después extiende el programa a una lista de veinte valores y calcula la diferencia acumulada entre los dos métodos.
5. Formato de salida. Modifica la versión con Float del conversor de temperatura para que produzca una tabla con tres columnas: Fahrenheit, Celsius y Kelvin. Ajusta Fore y Aft hasta que las tres columnas queden perfectamente alineadas y ningún valor se desborde.
6. Tipos que no se mezclan. Extiende Temperatura_Tipada con un tercer tipo Grados_K. Escribe las funciones de conversión entre los tres. Después intenta deliberadamente sumar un Grados_C con un Grados_F y transcribe el mensaje de error del compilador.
7. Subtipos con rango. Declara subtype Kelvin_Valido is Float range 0.0 .. Float'Last; y escribe un procedimiento que reciba un valor y lo asigne a una variable de ese subtipo. Prueba con un valor negativo y captura la excepción resultante, imprimiendo el nombre con Ada.Exceptions.Exception_Name.
8. Bucle sin while. Reescribe el conversor de temperatura usando un bucle for sobre un rango entero, calculando el valor de Fahrenheit dentro del cuerpo. Compara la legibilidad con la versión while y explica en qué casos cada forma resulta más directa.
9. Genéricos. Declara type Real is digits 18;, instancia Ada.Numerics.Generic_Elementary_Functions para ese tipo y calcula la raíz cuadrada de 2 con la máxima precisión que soporte tu máquina. Compara el resultado con el de Float e indica a partir de qué decimal difieren. Si el compilador rechaza los 18 dígitos, baja el número hasta que acepte y anota el máximo alcanzado.
10. Excepciones anidadas. Escribe un programa con tres niveles de subprogramas donde el más interno eleve una excepción propia declarada con Mi_Error : exception;. Captúrala en el nivel intermedio, imprime un mensaje y vuelve a elevarla con raise;. Captúrala finalmente en el nivel superior. Describe el orden en que aparecen los mensajes.
11. Código de salida. Modifica raiz_interactiva.adb para que, usando Ada.Command_Line.Set_Exit_Status, termine con estado distinto de cero si el usuario ingresó al menos un valor inválido. Verifica el resultado con echo $? en la terminal y relaciónalo con lo que vimos sobre procesos y su valor de retorno.
12. Comparar con C. Escribe el mismo conversor de temperatura en C. Compila ambos con optimización y compara el tamaño del ejecutable, el tiempo de compilación y la cantidad de llamadas al sistema que reporta strace -c en cada uno. Explica las diferencias en función de lo que sabes del runtime de cada lenguaje.
Qué sigue
En este capítulo cubrimos el punto de partida completo de Ada: de dónde viene el lenguaje y qué problema organizacional buscaba resolver, cómo se instala GNAT con el gestor de paquetes o con Alire, qué significa cada uno de los archivos de un proyecto, y qué ocurre realmente en las tres etapas de compilar, ligar y enlazar. Sobre esa base escribimos tres programas y los leímos línea por línea: el hola mundo nos dio la estructura mínima de una unidad y la diferencia entre with y use; la raíz cuadrada nos dio subprogramas con parámetros, atributos como 'Image, excepciones y la separación entre especificación y cuerpo; el conversor de temperatura nos dio bucles, salida formateada, la trampa de la división entera y el tipado fuerte aplicado a unidades físicas.
Lo que todavía no podemos hacer es manejar colecciones de datos. Todos nuestros programas trabajan con valores sueltos, y en cuanto se necesita procesar una serie de mediciones, el texto de una línea de comandos o el contenido de un archivo, hacen falta arreglos y cadenas de verdad. Ada tiene un tratamiento de arreglos poco habitual: los índices pueden empezar en cualquier valor y ser de cualquier tipo discreto, el largo forma parte del tipo del objeto, y los cortes son una construcción del lenguaje. Las cadenas heredan todo eso, lo que explica por qué existen tres familias distintas de String y cuándo conviene cada una.
En el capítulo 13 entramos en ese terreno: arreglos de una y varias dimensiones, arreglos con y sin restricción de tamaño, atributos 'First, 'Last, 'Length y 'Range, las tres formas de cadena, y el uso de Ada.Command_Line para recibir argumentos desde la terminal, que es el punto donde nuestros programas empiezan a conversar con el sistema operativo. Puedes revisar el recorrido completo del curso en el índice.