Nimony emerge como una evolución ambiciosa dentro del ecosistema del lenguaje Nim, preparando el camino para lo que será Nim 3.0. Más que una simple réplica de Nim 2, Nimony introduce una versión optimizada y hermosa del lenguaje que, aunque todavía en desarrollo, ya resulta sumamente útil por derecho propio. Su enfoque central es dotar a Nim de capacidades para sistemas embebidos y de tiempo real, haciendo hincapié en la seguridad de memoria y en la predictibilidad del código generado. Esta aproximación abre un abanico de posibilidades para desarrolladores que requieren control estricto sobre el rendimiento y la eficiencia, sin renunciar a la expresividad y concisión modernas.
En los sistemas embebidos, garantizar que las operaciones se ejecuten en tiempos máximos definidos (WCET) es crucial. Por esta razón, Nimony evita mecánicas como compiladores just-in-time y recolectores de basura con seguimiento, ya que generan imprevisibilidad en tiempos de ejecución. En su lugar, opta por una representación directa de datos, donde los tipos primitivos —como enteros y caracteres— mapean exactamente a palabras o bytes de máquina. De igual manera, los tipos complejos se estructuran sin indireccionamientos, lo que significa que un objeto con varios campos se almacena contiguamente, agilizando el acceso y mejorando la eficiencia en la pila o en estructuras que anidan estos objetos. Uno de los pilares en Nimony es la gestión automática de memoria, diseñada para favorecer la seguridad y evitar errores tan comunes como el uso de memoria liberada.
Adoptando un modelo basado en destructores y semánticas de movimiento, similar a lo que ofrecen Nim 2, Rust y C++, Nimony simplifica las opciones existentes dejando como única opción la gestión con mm:atomicArc. Este modelo permite una administración de recursos que se adapta muy bien a distintos tipos de objetos, incluidos canales que involucran recursos del sistema operativo. A diferencia de recolectores de basura que generan finalizadores impredecibles o la gestión de regiones que retienen objetos más tiempo del necesario, esta aproximación asegura una composición natural y predecible de la memoria y los recursos, facilitando el desarrollo de software robusto y eficiente. En relación con el manejo de errores, Nimony adopta un posicionamiento particular con respecto a las tendencias modernas que privilegian la evitación de excepciones mediante tipos suma y patrones de coincidencia. Aunque incorpora el manejo tradicional de excepciones de Nim, lo hace con la novedad de que toda rutina que pueda lanzar una excepción debe estar claramente anotada con {.
raises.}, haciendo explícito el control oculto de flujo que esto implica. Sin embargo, el autor manifiesta una preferencia por integrar el estado de error dentro de los mismos objetos; por ejemplo, flujos que representen un estado de error, números flotantes que puedan ser NaN o valores enteros específicos indicativos de invalidez. Esta perspectiva reduce la necesidad de manejar errores de manera externa o interruptiva, y convierte a la detección y manejo de errores en una parte natural del modelo de datos y flujos de la aplicación. Para complementar, el uso del enum tipo seguro ErrorCode ofrece una manera unificada y eficiente de propagar errores entre librerías y servicios, facilitando una representación clara y concisa de errores comunes como RangeError o OutOfMemError sin incurrir en asignación de memoria adicional.
La integración de ErrorCode con códigos de error de sistemas POSIX, Windows y status HTTP es particularmente relevante para aplicaciones de servicios y redes, que pueden mapear errores internos a estados reconocidos por protocolos como HTTP, elevando la interoperabilidad y calidad del software resultante. Nimony también aborda con especial cuidado el fenómeno de falta de memoria (OOM). En lugar de asumir que cualquier error por OOM debe derivar en una terminación abrupta del programa, se promueve una gestión más matizada y flexible. Los contenedores que no puedan asignar memoria llaman a un oomHandler personalizable que puede memorizar la magnitud del fallo y permitir que la ejecución continúe, ofreciendo utilidades para inspeccionar si hubo un fallo de memoria sin necesidad de detener la aplicación. Esta estrategia pragmática reduce riesgos en sistemas críticos y posibilita respuestas más inteligentes ante situaciones de restricción de recursos.
El enfoque de Nimony para la construcción de objetos refuerza esta filosofía. Los procedimientos que pueden fallar al asignar objetos retornan valores nil, pero el compilador exige que este caso sea tratado explícitamente, disminuyendo errores asociados al nulo. En los procedimientos anotados con {.raises.} la comprobación de nil puede ser mapeada automáticamente a una señalización de error OutOfMemError, simplificando el código y manteniendo la seguridad.
Así, el sistema mantiene un equilibrio saludable entre rigor, seguridad y productividad del desarrollador. La implementación de genéricos en Nimony también recibe atención especial. Más allá de simplemente comprobar tipos en las instanciaciones, la comprobación exhaustiva durante la definición del genérico permite detectar errores anticipadamente y brinda a los entornos de desarrollo una capacidad superior de autocompletado e inspección. Se mantienen conceptos esenciales de Nim, como la definición de restricciones a través de conceptos para tipos genéricos (p. ej.
, Fibable) que establecen las operaciones mínimas requeridas para que el código genérico funcione correctamente. Este diseño eleva la calidad y la fiabilidad del código, facilitando la construcción de librerías y contenedores personalizados que pueden reemplazar el uso de estructuras integradas. En cuanto a la programación concurrente y paralela, Nimony apuesta por unificar estos dos paradigmas aparentemente distintos mediante un único constructo llamado spawn. Esta función, gestionada a nivel del scheduler, decide en tiempo de ejecución si la tarea lanzada se ejecuta de manera asíncrona en el mismo hilo o en otro diferente. Esta unificación elimina la fricción habitual entre programación multihilo y programación asíncrona, obligando al mismo tiempo a que los argumentos pasados en spawn cumplan con las restricciones de seguridad para hilos, promoviendo así código concurrente seguro.
La transformación del código a un estilo continuation passing style (CPS) es clave para facilitar esta concurrencia sin que el programador necesite gestionar la complejidad añadida. Además, el paralelismo puro puede implementarse de forma más sencilla, ya que spawn se reduce a enrutar tareas hacia un pool de hilos, evitando transformar completamente el código. Este diseño híbrido permite aprovechar las fortalezas de ambos modelos de ejecución sin sacrificar rendimiento ni legibilidad. Un ejemplo concreto de esta filosofía puede verse en la implementación del clásico algoritmo Fibonacci en paralelo. En lugar de depender del spawn explícito para ambas llamadas recursivas, Nimony introduce el concepto de ciclo for paralelo (||) que garantiza ejecuciones paralelas seguras y estructuradas, evitando problemas de sincronización y permitiendo que la última iteración se realice en el hilo llamador.
Este abordaje facilita la programación de tareas paralelas en loops y es especialmente útil en áreas como computación científica o programación GPU, donde las variables de flujo y operaciones atómicas pueden complicar el desarrollo. La metaprogramación también recibe un impulso significativo en Nimony mediante los plugins, que representan una evolución avanzada de las macros tradicionales de Nim. Estas extensiones, que se compilan a código máquina y poseen acceso completo a la información tipada, ejecutan transformaciones sobre el código de forma eficiente, en paralelo y de manera incremental. Esta capacidad permite desde la simple generación automática de código hasta transformaciones complejas, como el método spawn implementado con un plugin de tipo módulo que reestructura funciones para facilitar la concurrencia sin requerir anotaciones explícitas adicionales. Aun más sofisticado es el uso de plugins aplicados a tipos nominales, capaces de reescribir código en la compilación para optimizar operaciones y evitar la creación de objetos temporales innecesarios.