Desde que revelamos por primera vez el protocolo Qubic se nos ha pedido que entremos en más detalles sobre el funcionamiento interno del Modelo de Computación. Esta es la primera, en una serie de entradas, que intentará hacer exactamente eso. ¡Así que abróchate el cinturón, va a ser un viaje interesante!
Hay mucho terreno por cubrir, y por ahora vamos a ignorar los conceptos de protocolo Qubic de alto nivel como Assembleas, Quórums, o incluso el Tangle de IOTA. En lugar de eso, primero veremos cómo se están ejecutando los programas Qubic usando una especificación funcional estructurada de lenguaje de flujo de datos llamada Abra, y discutiremos los conceptos básicos usados en ella.
En futuras entregas examinaremos Qupla, un lenguaje de programación de Qubic, que es la primera implementación de la especificación Abra, y veremos las entidades básicas de Qupla, sus funciones, expresiones, operaciones y algunos ejemplos de programación. A continuación examinaremos la parte de un nodo habilitado para Qubic (Q-node) que inicia y facilita el procesamiento real de una tarea qubic: el Supervisor Qubic. Mostraremos cómo el Supervisor está estrechamente entrelazado con el modelo de procesamiento de Abra.
Abra: una especificación de lenguaje de flujo de datos funcional.
Descargo de responsabilidad: la especificación de Abra aún no está 100% completa. Podemos introducir conceptos y cambios adicionales que creemos que son útiles mientras intentamos usar la especificación para tareas del mundo real. Pero en general los conceptos son claros. Actualmente tenemos un intérprete y compilador, Qupla, en funcionamiento que implementa la especificación Abra.
Abra especifica un conjunto de instrucciones generales extremadamente mínimo que está diseñado para proporcionar un mapeo natural de los programas funcionales basados en el flujo de datos a los tipos de hardware más variados operados por los dispositivos del IoT (en español internet de las cosas). Esto significa que se puede asignar fácilmente para que se ejecute en cualquier dispositivo, desde CPUs, GPUs, FPGAs y ASICs. Sin embargo, Abra está principalmente orientada a la creación de circuitos FPGA y ASIC. Se espera que en el futuro una gran parte de todos los dispositivos de IoT funcionen con este tipo de hardware, y los dispositivos de propósito general de CPU/GPU se utilizarán principalmente para soluciones PoC y/o soluciones de servidor destinadas a los seres humanos. Tenga en cuenta que en estos artículos hablaremos de CPU cuando nos referimos a CPU/GPU y de FPGA cuando nos referimos a FPGA/ASIC.
Para resumir antes de zambullirse: Abra es único en el sentido de que emplea lógica trinaria y soporta directamente wave-pipelining (en español canalización de ondas). Estos son dos aspectos que le permiten maximizar la eficiencia del código asociado, lo que es importante para los dispositivos del IoT debido a sus limitados recursos energéticos y de procesamiento. Se ha demostrado que la canalización de onda de los circuitos combinatorios alcanza velocidades de reloj de 2 a 7 veces mayores que las posibles para los mismos circuitos con la canalización convencional. Además, el uso de circuitos trinarios puede resultar en hasta un 50% más de eficiencia energética debido a la representación más densa de los valores. Tenga en cuenta que estos circuitos pueden incluso crearse con puertas NAND binarias tradicionales.
No dejes que el tecno-balbuceo de este resumen te asuste. Le llevaremos de la mano a través de los conceptos y poco a poco nos iremos formando a partir de las partes más pequeñas. Comencemos con la diferencia más visible con la mayoría de las especificaciones de idiomas: Abra especifica el código trinario y los datos.
Código y datos binarios vs trinarios.
Los sistemas binarios utilizan bits para representar código y datos, donde cada bit puede asumir sólo uno de los dos valores posibles. Los valores más grandes se pueden representar como números binarios utilizando una serie de bits.
Del mismo modo, los sistemas trinarios utilizan los llamados trits para representar el código y los datos, donde cada trit puede asumir sólo uno de los 3 valores posibles. Los valores más grandes se pueden representar como números trinarios utilizando una serie de trits.
Abra es un sistema trinario. Los sistemas trinarios requieren menos cableado porque los valores se pueden representar aproximadamente 1,58 veces más eficientemente, lo que se traduce en una reducción del consumo de energía.
Abra sólo soporta un único tipo de datos nativos: el vector Trit. Un vector trit es una serie consecutiva de trits. Eso es todo. Los vectores de trits siempre tienen un tamaño fijo. Incluso un solo trit está representado por un vector trit, es decir, un vector trit que tiene el tamaño 1. No hay manera de definir vectores trit de tamaño variable en Abra.
Nótese que la interpretación del significado de cada trit en un vector trit se deja completamente en manos de la implementación o del programador. La especificación Abra es totalmente agnóstica en ese sentido. Todo lo que hace es especificar cómo fluyen los datos y cómo transformarlos para calcular los resultados de una manera que se asemeje a cualquier hardware.
También tenga en cuenta que esto significa que en Abra no hay ninguno de los tipos de datos habituales de mapeo a límites de (multi)bytes dependientes de la arquitectura como los que se encuentran en los lenguajes de programación tradicionales. El programador es libre de seleccionar cualquier tamaño de vector trit, y el código generado reflejará este tamaño exactamente. Por supuesto, siempre es posible que un programador seleccione tamaños de vectores trit que se aproximen a los tipos de datos de tamaño (multi)byte habituales cuando se dirige a hardware específico, pero eso requerirá un conocimiento íntimo de cómo se mapearán los vectores trit a ese hardware específico.
Limitar el tamaño del tipo de datos para que coincida con los rangos de valores que se utilizan realmente suele reducir las necesidades de energía al limitar la cantidad de circuitos que realmente se necesitan en las FPGAs. Imagine una variable que sólo necesita poder representar los valores 0-10. Con la mayoría de los lenguajes de programación tradicionales necesitaría usar una variable de 8 bits (0 a 255) como mínimo, donde 4 bits (0 a 15) habrían sido suficientes, y el hardware está diseñado para manipular bytes a la vez de todas formas. Con Abra puede bastar con un vector trit de 3 tramos (0 a 26), y en FPGA sólo elaboraras a partir de los circuitos necesarios para manipular 3 tramos.
Transformación mediante tablas de consulta.
En el corazón de Abra hay un tipo muy especial de construcción global estática llamada tabla de búsqueda (LUT). Un LUT es lo que se utiliza para transformar los valores de los datos de entrada en valores de salida.
Se puede ver una LUT como una instrucción muy general, que se puede programar para realizar todo tipo de operaciones. Debido a esto, no hay necesidad real de un conjunto más complejo de instrucciones predefinidas, como ocurre con la mayoría de los lenguajes de nivel intermedio o de ensamblador. Un LUT puede emular los resultados de dichas instrucciones directamente, o se pueden utilizar varios LUTs combinados con alguna lógica para crear un efecto equivalente. Y los LUTs tienen una ventaja que la mayoría de las instrucciones’normales’ no tienen: pueden participar en el flujo de datos. Esto significa que no necesitan una señal de reloj externa para poder funcionar. Esta es la clave para un lenguaje que soporta el wave-pipelining. El único’tictac del reloj’ lo proporciona el Supervisor cuando proporciona datos de entrada para procesar.
Nótese que de nuevo, al igual que con los vectores trit, depende de la implementación o del programador definir el significado exacto de LUTs y la especificación de Abra es completamente agnóstica a eso. Todo lo que hace es especificar cómo fluyen los datos a través de un LUT y cómo transforma los datos para calcular los resultados de una manera que se mapeará a cualquier hardware.
LUTs en Abra puede tomar hasta 3 trits de entrada. El programador puede especificar explícitamente para cada combinación única de valores trit de entrada qué valor trit de salida individual se generará. Para cualquier combinación no especificada de valores de entrada, o cuando cualquier trit de entrada es nulo, el LUT devolverá nulo. Recuerde, nulo es la ausencia de flujo de datos. La importancia de esto se discutirá en la sección sobre operaciones de fusión. Pero primero veremos la anatomía del código de Abra.
La disposición de la unidad de código Abra.
Una unidad de código Abra consiste en una secuencia de bloques que se divide en 3 sub-secuencias:
- Una secuencia de bloques de definición LUT. Un bloque de definición LUT contiene 27 entradas, una para cada una de las posibles combinaciones únicas de 3 tramos de entrada. Cada entrada especifica el valor de trit de salida para la combinación de entrada correspondiente, o un indicador nulo para combinaciones no definidas. Para ser claros, cada una de las 27 entradas en el bloque de definición de LUT puede asumir 4 valores diferentes que indican cómo la implementación necesita mapear esa combinación de entrada a una salida trit o nula.
- Una secuencia de bloques de definición de rama. Un bloque de definición de rama es una secuencia de centros (instrucciones) que forman una única unidad de ejecución llamada rama. Hablaremos de las sucursales en la siguiente sección.
- Una secuencia de bloques de referencia externos. Los bloques de referencia externos se refieren a bloques en otras unidades de código Abra. Cada bloque de referencia externo especifica el hash de la unidad de código Abra específica y una secuencia de índices de los bloques de esa unidad de código a la que se refiere. Así es como Abra puede tener unidades de biblioteca que contienen código reutilizable.
Una unidad de código Abra es completamente autónoma y en el contexto de Qubic será almacenada como un único bloque de datos trinario codificado que puede ser enviado como carga útil en un mensaje IOTA. La unidad de código puede ser identificada únicamente por su valor hash. Esto hace imposible manipular las unidades de código una vez que están almacenadas públicamente. Una unidad de código Abra y cualquier otra unidad de código a la que se haga referencia siempre será un conjunto autoconsistente, comenzando con el hash de la unidad «principal».
Anatomía de una rama.
Una rama es similar a una función, ya que se utiliza en la mayoría de los lenguajes de programación. Pero como verá, también hay algunas diferencias importantes.
Una rama es una secuencia de instrucciones Abra llamadas sitios. Los sitios describen las salidas de sus instrucciones Abra asociadas. Son similares a las variables locales temporales pero no ocupan espacio de almacenamiento. Sería mejor verlas como etiquetas que hacen referencia a su salida de instrucción asociada. Los sitios pueden ser utilizados como entradas a una o más instrucciones de otro sitio. En las implementaciones de FPGA los sitios serán conectados directamente como entradas a uno o más sitios. Un centro sólo puede utilizar centros que ya hayan generado un valor como entradas (flujo de datos). La dirección del gráfico que se crea de esta manera es siempre hacia adelante, en la dirección del flujo de datos. Los sitios son referenciados por su índice en la secuencia de sitios de la rama.
Una rama siempre toma un único vector trit de tamaño fijo como entrada y siempre devuelve un único vector trit de tamaño fijo como salida. Exactamente lo que estos vectores trit representan depende de la implementación o del programador como de costumbre. Una vez más, la especificación Abra es completamente agnóstica a eso, porque sólo especifica el flujo de datos.
Los emplazamientos de una rama se agrupan en 4 sub-secuencias:
- Sitios de entrada. Definen cómo se dividirá el vector trit de entrada de la rama en subvectores. De hecho, no son más que una secuencia de definiciones de tamaño. Cada sitio de entrada comienza donde el anterior quedó dentro del vector trit de entrada de la rama. Puede ver el vector trit de entrada como una concatenación de todos los subvectores definidos por los sitios de entrada. Tenga en cuenta que cuando el vector trit de entrada real es demasiado grande, el resto no utilizado será ignorado. Y cuando el vector trit de entrada real es demasiado pequeño, Abra asumirá que el resto de los tramos necesarios son nulos. Esto permite algunos comportamientos interesantes que discutiremos más adelante.
- Sitios de los cuerpos. Definen las operaciones intermedias necesarias para que la rama realice su función. Cada sitio está asociado o bien con una instruccion knot o merge. El vector trit de entrada a estas instrucciones se especifica mediante una secuencia de índices de sitio. La interpretación de esta secuencia depende del tipo de instrucción.
- Sitios de salida. Son exactamente iguales a los sitios de los cuerpos, excepto que definen las operaciones finales para completar la función de la rama. Los resultados de cada sitio de salida se concatenan en secuencia para formar el vector trit de salida de la rama.
- Sitios de cierre de memoria. Estos son exactamente los mismos que las zonas del cuerpo, pero tienen una función adicional especial. Son una manera para que una rama almacene el estado entre invocaciones. Se definen en último lugar, ya que los cierres de memoria sólo se actualizarán una vez que la rama haya realizado su función. Esto significa que, mientras se realiza la función de ramificación, cada vez que se hace referencia a un sitio de cierre de memoria, se utiliza el valor que el cierre de memoria tenía en el momento en que se invocó la función. El valor inicial de un bloqueo de memoria es siempre cero.
¿Recuerdas que dijimos que la dirección de la gráfica siempre era hacia adelante, es decir, el flujo de datos de la t.r.v.? Bueno, los sitios de cierre de memoria ya tienen un valor al invocar la rama, lo que significa que puede utilizar estos sitios como entradas en cualquier lugar aunque sólo estén definidos al final. Así que el flujo de datos sigue avanzando. Y sólo después de que la rama complete la ejecución sus valores de sitio serán calculados y bloqueados, lo que significa de nuevo que el flujo de datos es hacia adelante. Los sitios de captura de memoria sólo tienen esta forma de ser utilizados en 2 puntos diferentes del gráfico: una vez al principio como entradas y una vez al final como salidas.
Instrucción knot (nudo) de Abra.
La instrucción knot (nudo) se llama acertadamente, porque une las ramas entre sí. Es similar a un mecanismo de llamada de función pero con varias diferencias. Una instrucción knot toma una secuencia de índices de sitios de entrada y concatena esos sitios juntos en un vector trit de entrada que se pasa al bloque objetivo del nudo, el cual es especificado por un índice en la secuencia de bloques de la unidad de código. Sólo hay dos tipos de bloques a los que se puede pasar el vector trit concatenado:
- Un bloque LUT, en cuyo caso el tamaño del vector trit de entrada tiene que ser de 1, 2 ó 3 tramos y el sitio del nudo devolverá el único trit que está asociado con esa combinación específica de tramos de entrada para el bloque LUT referido.
- Un bloque de rama, en cuyo caso el vector trit de entrada puede ser de cualquier tamaño distinto de cero. El vector trit concatenado será pasado como vector trit de entrada a una nueva instancia de la rama y el sitio del nudo devolverá el vector trit de salida de la rama después de la invocación.
Tenga en cuenta que los bloques externos son simplemente referencias indirectas a bloques LUT o bloques de rama, por lo que no es necesaria una diferenciación real. Puede utilizarlos en nudos como si los estuviera haciendo referencia directamente.
Un nudo de rama no es una llamada de función en el sentido tradicional, con una pila de llamadas y una única instancia de la función que puede ser llamada desde cualquier lugar. En su lugar, invoca literalmente una nueva instancia de la rama a la que se refiere.
Es de ayuda si piensas en una rama como un bloque de circuitos. Puede conectarse a una sola entrada específica y a una salida específica. Por lo tanto, cualquier rama referenciada a través de un nudo necesita ser instanciada como un bloque de circuitos separado y entonces los sitios de entrada del nudo deben ser cableados a los sitios de entrada de la rama instanciada, y los sitios de salida de la rama instanciada deben ser cableados a todos los circuitos que usan el sitio del nudo como entrada.
Esta instanciación de ramas tiene algunas ramificaciones importantes cuando la rama está llena de estados porque utiliza cierres de memoria. Porque cada rama viene con su propio juego de cerraduras de memoria. Esto significa que para acceder al mismo estado de la rama es necesario seguir la ruta de instanciación original (o ruta de’llamada’), lo que requiere algún mecanismo especial que discutiremos con más detalle una vez que lleguemos a la descripción del Supervisor Qubic.
La instrucción merge (fusión) de Abra.
La instrucción merge toma uno o más sitios de entrada que todos deben ser del mismo tamaño. De todos estos sitios de entrada sólo uno puede evaluar a un valor no nulo. El sitio de fusión devolverá el valor de un solo sitio de entrada no nulo, o nulo cuando todos los sitios de entrada estén evaluando a nulo. El valor de retorno es siempre del mismo tamaño que cada sitio de entrada. Tenga en cuenta que tener más de un sitio de entrada no nulo se considera un error de programación y provocará que se desencadene una excepción y que la ejecución de la rama se detenga sin resultado.
Tubo de conexión en estrella triple
Puede visualizar la instrucción de fusión como un tubo de conexión en estrella, donde un número de tubos de entrada se unen en un solo tubo de salida. Imagine que cada tubo de entrada está conectado a una fuente diferente de fluidos químicos volátiles. Mientras no haya ninguna derivación de fuente abierta (todas las entradas nulas) no saldrá nada de la tubería de salida (salida nula). Si sólo se abre una toma de la fuente (una entrada no nula), el fluido químico correspondiente fluirá sin impedimentos a través de la conexión a la tubería de salida sin problemas (salida no nula, mismo valor). Pero si se abren múltiples tomas de fuentes (múltiples entradas no nulas), sus productos químicos se mezclarán en la fusión y causarán una explosión (excepción).
La instrucción merge realizará una función clave en la creación de la lógica de decisión. Esencialmente, puede configurar varias vías de ejecución paralelas (flujos de datos) en una rama. Los resultados de estas rutas terminan como diferentes sitios de entrada en la instrucción de fusión, donde sólo el resultado deseado ‘sobrevive’. Debido a que es esencial que al máximo una ruta de ejecución evalúe a un valor no nulo, se necesita una forma de introducir estos valores nulos. Ahí es donde los LUTs son útiles. En cada ruta de ejecución se puede utilizar un LUT como filtro que sólo permite que los datos fluyan cuando se cumplen determinadas condiciones. Sólo hay que tener cuidado de que las condiciones en las que se filtran los LUTs sean mutuamente excluyentes en cada ruta de ejecución.
Por cierto, este es el único mecanismo que nos permite tomar una decisión en Abra. Abra no tiene ninguna de las sentencias de flujo de control clásicas. Esto significa que no hay sentencias de decisión similares a las de if-then-else, en las que sólo se toma una única ruta de ejecución dependiendo de una condición y se omite la otra, ni hay sentencias de looping como las de for-loops y while-loops. Tales construcciones impedirían el flujo de datos. La ausencia de estas construcciones también hace que sea fácil mantener puras las funciones y nos permite razonar sobre su corrección al mirarlo desde el punto de vista de una única invocación de una rama.
Incluso con la cantidad limitada de mecanismos descritos hasta ahora, es posible crear una construcción en bucle. Puede configurar una rama que tenga dos rutas de ejecución, una filtrada en la condición de continuación de bucle y otra en la condición de final de bucle. La primera ruta llamaría a una rama que ejecuta una iteración y luego llama a la función de bucle recursivamente. La segunda ruta llamaría a una función de continuación que reanuda la ejecución una vez finalizado el bucle.
Por supuesto, la recursividad no siempre es la mejor solución, especialmente cuando se trata de ramas que se instancian cada vez que se llaman. Para poder instanciar una rama, todo lo que es llamado por esa rama necesita ser instanciado también. Esto podría convertirse rápidamente en una cantidad prohibitivamente grande de circuitos, dependiendo de la profundidad de la recursión. Por esta razón, y también para asegurarse de que es imposible entrar en un bucle sin fin, Abra requiere que especifiques una profundidad máxima de invocaciones de rama para una unidad de código. Cuando se alcanza este límite de profundidad, en lugar de invocar otra rama Abra volverá nula para esa invocación, por lo que el flujo de datos se detendrá.
Hasta ahora sólo hemos mirado el código Abra desde el punto de vista de una única invocación de una rama. Pero, por supuesto, es posible invocar una rama varias veces. De lo contrario, no tendría mucho sentido tener la posibilidad de almacenar el estado en los cierres de memoria. Eso sólo tiene sentido frente a las múltiples invocaciones. Así que aquí es donde empezaremos a introducir conceptos que facilitan la interacción con el Supervisor Qubic.
Flujo de datos reactivo en Abra.
El flujo de datos en Abra es reactivo. Esto significa que las entidades computacionales responden a cambios discretos en los datos de entrada recalculando los resultados de todas las entidades que dependen de estos datos de entrada. Llamamos a este tipo de evento en el que los datos de entrada cambian un efecto. Un efecto siempre se envía directamente a un entorno. Un efecto puede ser procesado por cualquier entidad computacional. Para que un efecto desencadene el procesamiento por parte de una entidad, es necesario que esta entidad se una al entorno correspondiente. Al unirse a un entorno, la entidad indica que desea que se le notifique siempre que se produzcan efectos en ese entorno, de modo que pueda procesar los datos del efecto. Llamamos a este mecanismo EEE (por Environment, Entity, Effect) y gobierna la forma en que las entidades separadas interactúan entre sí.
Lo que es importante notar es que una onda es una unidad atómica de procesamiento. Esto significa que con Abra no utilizamos la programación de tareas preventiva con sus gastos generales de cambio de contexto asociados. El final de una onda forma un punto de conmutación de contexto natural. La naturaleza atómica de una onda también significa que hay una necesidad muy reducida de sincronización porque no hay que tener en cuenta nada que pueda interrumpir una onda.
Los datos de los resultados de salida producidos por una entidad pueden enviarse como un efecto a uno o más entornos para su posterior procesamiento en el siguiente ciclo (a menos que se hayan marcado explícitamente para que se retrasen). Esto significa que una onda puede desencadenar una cascada de otras ondas, que continuarán hasta que se alcance una nueva situación estable (es decir, no hay más efectos de cola pendientes que procesar en el ciclo actual). Tal cascada de ondas, que se inicia con la llegada de un efecto, es lo que llamamos un cuanto. Puesto que la cantidad exacta de fases en un cuanto y lo que sucede durante estas fases depende de la implementación, un cuanto no tiene un período de tiempo fijo asociado con él. Pero encarna un conjunto completo y consistente de cálculos.
Dentro de un cuanto, los únicos efectos que pueden desencadenar una nueva onda son los efectos generados por los cálculos dentro del propio cuanto. Cualquier efecto externo que se produzca mientras el tratamiento tiene lugar dentro de un cuanto se pospone hasta el inicio del cuanto siguiente. Además, los efectos internos pueden retrasarse cualquier número de cuantos a propósito.
Tenga en cuenta que Abra evita una cascada interminable de ondas al requerir que las entidades especifiquen un límite de invocación. Cuando se ha alcanzado ese límite dentro de un cuanto, cualquier efecto que pueda causar que se exceda el límite de invocación se pospondrá hasta el comienzo del siguiente cuanto. Esto garantiza que un cuanto no puede entrar en un bucle de invocación interminable. Al comienzo del siguiente cuanto se restablecerán los contadores de invocación y se podrá reanudar el procesamiento de los efectos aplazados.
Las aplicaciones con componentes de tiempo crítico pueden utilizar este límite de invocación para reducir la longitud de un cuanto, o pueden especificar una cantidad específica de cuantos para retrasar la reanudación de la invocación. Esto permite que sus componentes de tiempo crítico puedan reparar sus efectos (externos) de manera oportuna.
Nótese que podemos usar EEE para lograr el looping en Abra haciendo que la entidad genere un efecto a un entorno al que se ha unido, lo que significa que la misma entidad será activada de nuevo por el efecto que ha generado. Los datos asociados al efecto pueden utilizarse como datos de entrada para la siguiente iteración. El estado de iteración se envía como parte de los datos del efecto. El looping de esta manera es mucho más controlado, ya que cada iteración se realiza en una sola onda. También está limitado por los límites de invocación, lo que significa que la ejecución de bucles con grandes cantidades de iteraciones puede extenderse a varios cuantos. Tenga en cuenta que esto también significa que los bucles infinitos son definitivamente posibles de esta manera, pero como ahora están gobernados por el Supervisor, nunca podrán bloquear el sistema.
También tenga en cuenta que invocar una entidad a través de EEE significa que comienza de nuevo en la parte superior de la ruta de instanciación de la rama, lo que significa que a través de una programación cuidadosa puede asegurarse de terminar en una rama que previamente almacenada en la memoria se bloquea y procesa el efecto en el contexto de ese estado.
Las interacciones entre el Supervisor Qubic, las sucursales de Abra, otras entidades y la forma en que los efectos son generados e interactúan con los ambientes serán discutidos en profundidad después de nuestra discusión sobre cómo Qupla implementa Abra. Para facilitar esta discusión, primero presentamos un pseudo-código legible para Abra.
Pseudo-código de Abra.
Hemos ideado un pseudo-código para Abra para que podamos tener instrucciones legibles para el ser humano. Cada tipo de bloque en la unidad de código tiene su propia disposición específica.
// External reference block extern <unit name> hash <unit hash> blocks <blocks>
// <unit name> is the name of the external code unit and only // present for human readability. // <unit hash> is the 81-tryte hash value of the external code unit. // <blocks> is a comma-separated list of blocks to import from // the external code unit.
Tenga en cuenta que todavía no hemos utilizado bloques externos porque por ahora todo se encuentra en una sola unidad de código, y lo anterior probablemente no será la notación final. Sólo se menciona aquí para que esté completo.
// LUT definition block lut <table> <name>
// <table> is the look-up table string of 27 entries consisting // of 0, 1, -, and @ (the latter indicating null). // <name> is a symbolic name for the look-up table that can be // used to to reference it by. Which is easier to use // than the block index it actually represents.
// Examples: // -01-01-01-01-01-01-01-01-01 // input trit 0 // ---000111---000111---000111 // input trit 1 // ---------000000000111111111 // input trit 2 lut @10@10@10@10@10@10@10@10@10 not_0 lut 100010001100010001100010001 equal_0
La tabla de salidas LUT está ordenada por combinación de entradas desde—,0–,1–, hasta -11, 011, 111 (contando así de -13 a +13, en codificación trinaria balanceada big-endian). Note que debido a que siempre tenemos 27 valores en la tabla, 1 entrada trit LUTs tendrá sus 3 valores replicados 9 veces, y 2 entradas trit LUTs tendrán sus 9 valores replicados 3 veces.
// Branch definition block branch <name> [<site size>] in <site name> // 1 or more input [<site size>] <site name> = <operation> // 0 or more body [<site size>] out <site name> = <operation> // 1 or more output [<site size>] latch <site name> = <operation> // 0 or more latch
// <name> is the name of the branch, often encoding a return // length and sometimes an offset or other value in the // name to differentiate between different versions. // <site size> is the size of the site's trit vector. // This is only really necessary for input sites, but // it increases understanding of the code immensely. // <site name> is the name of the site, usually p<index> where // <index> is the number of the site within the // branch, starting at zero. But it can also be // a parameter/variable name from the implementation. // <operation> is a <lut knot>, <branch knot>, or <merge> operation // note how each operation employs different braces.
// Lut knot operation <lut name> [ <inputs> ]
// Branch knot operation <branch name> ( <inputs> )
// Merge operation merge { <inputs> }
// <inputs> is a list of site names. We use names instead of indices // because that helps understanding the code better
// Example: branch cmp_3 // compare two 3-trit vectors [ 3] in lhs // 1st 3-trit vector [ 3] in rhs // 2nd 3-trit vector [ 1] p2 = slice_1_0(lhs) // take 1st trit of 1st vector [ 1] p3 = slice_1_0(rhs) // take 1st trit of 2nd vector [ 1] val0 = cmp_0[p2, p3] // compare them [ 1] p5 = slice_1_1(lhs) // take 2nd trit of 1st vector [ 1] p6 = slice_1_1(rhs) // take 2nd trit of 2nd vector [ 1] val1 = cmp_0[p5, p6] // compare them [ 1] p8 = slice_1_2(lhs) // take 3rd trit of 1st vector [ 1] p9 = slice_1_2(rhs) // take 3rd trit of 2nd vector [ 1] val2 = cmp_0[p8, p9] // compare them [ 1] out ret = sign_0[val0, val1, val2] // output the comparison sign
La descripción anterior se mantiene deliberadamente concisa. Encontrarás que las instrucciones de Abra son tan sencillas que entender lo que está sucediendo es muy sencillo.
Conclusión
La especificación Abra define el flujo de datos al tiempo que se mantiene en gran medida agnóstica de la implementación mediante el uso de un conjunto muy limitado de instrucciones de bloques de construcción. Abra apoya un mecanismo cooperativo multitarea. Este mecanismo trabaja en conjunto con el Supervisor Qubic que discutiremos en un artículo futuro. Describimos una notación de pseudo-código para las instrucciones de Abra que nos ayudará a tener una idea de la especificación de Abra en sí para que todas las partes se junten bien. En la siguiente parte de esta serie profundizaremos en el lenguaje de programación Qupla, que implementa la especificación Abra.