Da quando è stato rivelato per la prima volta il protocollo Qubic è stato chiesto di approfondire i dettagli sul funzionamento interno del modello computazionale Qubic. Questo è il primo, in una serie di post, che cercherà di fare esattamente questo. Quindi allacciate le cinture di sicurezza, sarà un viaggio interessante!

C’è un sacco di terreno da coprire, e per ora ignoreremo i concetti del protocollo Qubic di livello superiore come Assemblies, Quorums, o anche il Tangle IOTA. Invece, saranno prima di tutto esaminati come i programmi Qubic vengono eseguiti utilizzando una specifica del linguaggio di flusso dati funzionale strutturato chiamato Abra, e discuteremo i concetti di base usati in Abra.

Nelle prossime puntate sarà esaminato Qupla, un linguaggio di programmazione Qubic, che è la prima implementazione della specifica Abra, e si darà un’occhiata alle entità di base, funzioni, espressioni, operazioni ed alcuni esempi di programmazione di Qupla. Dopo di che si esaminerà la parte di un nodo abilitato a Qubic (Q-node) che avvia e facilita l’effettiva elaborazione di un compito qubic: il Supervisore Qubic. Si mostrerà come il Supervisore sia strettamente legato al modello di elaborazione di Abra.

Si consiglia ai lettori che terminano questa serie di rileggerla nella sua interezza per una comprensione più profonda. Ciò che sembra complesso in un primo momento diventerà più chiaro durante la seconda lettura. C’è un’enorme quantità di nuovi concetti e paradigmi che è necessario trasmettere e si farà il possibile per mantenerla il più chiaro possibile, ma gli strumenti migliori per l’apprendimento e la comprensione sono la ripetizione e la sperimentazione. Quindi si invita a controllare il repository Qupla e di giocare con esso.

Abra: una specifica funzionale del linguaggio del flusso di dati

Disclaimer: la specifica Abra non è ancora completa al 100%. Saranno introdotti ulteriori concetti e modifiche che saranno ritenute utili mentre si chercherà di utilizzare le specifiche per compiti reali. Ma nel complesso i concetti sono chiari. Attualmente si ha un interprete e compilatore Qupla che implementa le specifiche Abra.

Abra specifica un set di istruzioni generali estremamente minimo che è stato progettato per fornire una mappatura naturale dei programmi funzionali basati sul flusso di dati ai vari tipi di hardware gestiti da dispositivi IoT. Ciò significa che può essere facilmente mappato per essere eseguito su qualsiasi dispositivo, dalle CPU, alle GPU, agli FPGA, agli ASIC. Tuttavia, Abra è principalmente orientato alla creazione di circuiti FPGA e ASIC. Si prevede che in futuro gran parte di tutti i dispositivi dell’IoT funzioneranno su questo tipo di hardware, ed i dispositivi CPU/GPU per uso generale saranno utilizzati principalmente per soluzioni PoC e/o soluzioni server che si rivolgono agli umani. Si noti che in questi articoli si parlerà di CPU quando si intende CPU/GPU e di FPGA quando intendiamo FPGA/ASIC.

Per riassumere prima di tuffarsi: Abra è unico in quanto utilizza una logica trinaria e supporta direttamente il wave-pipelining. Questi sono due aspetti che gli permettono di massimizzare l’efficienza del codice associato, aspetto importante per i dispositivi IoT a causa della loro energia e risorse di elaborazione limitate. È stato dimostrato che la segmentazione ad onda di circuiti combinatoriali raggiunge velocità di clock da 2 a 7 volte superiori a quelle possibili per gli stessi circuiti con la segmentazione convenzionale. Inoltre, l’uso di circuiti basati sulla tecnologia trinaria può portare ad un aumento fino al 50% dell’efficienza energetica grazie alla rappresentazione più densa dei valori. Si noti che tali circuiti possono essere creati anche con i tradizionali porte NAND binarie.

Sarete accompagnati per mano attraverso i concetti e saranno lentamente costruiti partendo dalle parti più piccole. Si inizia con la differenza più visibile dalla maggior parte delle specifiche linguistiche: Abra specifica il codice trinario ed i dati.

Codice e dati binari o ternari

I sistemi binari usano i bit per rappresentare codice e dati, dove ogni bit può assumere solo uno dei 2 valori possibili. Valori più grandi possono essere rappresentati con numeri binari utilizzando una serie di bit.

Allo stesso modo, i sistemi trinari usano i cosiddetti trits per rappresentare codice e dati, dove ogni trit può assumere solo uno dei 3 valori possibili. Valori più grandi possono essere rappresentati come numeri ternari utilizzando una serie di trits.

Abra è un sistema ternario. I sistemi ternari richiedono meno cablaggio perché i valori possono essere rappresentati circa 1,58 volte più efficientemente, il che si traduce in una riduzione del consumo energetico.

Abra supporta un solo tipo di dati nativo: il vettore trit. Un vettore trit è una serie consecutiva di trits. Questo è tutto. I vettori di trit hanno sempre una dimensione fissa. Anche un singolo trit è rappresentato da un vettore trit, vale a dire un vettore trit che ha dimensione 1. Non c’è modo di definire vettori trit a grandezza variabile in Abra.

Si noti che l’interpretazione del significato di ogni trit in un vettore trit è lasciata completamente all’implementazione o al programmatore. La specifica Abra è totalmente agnostica in questo senso. Tutto quello che fa è specificare i flussi di dati e come trasformarli per calcolare i risultati in un modo da mapparla a qualsiasi hardware.

Si noti anche che questo significa che in Abra non c’è nessuno dei soliti tipi di dati di mappatura dei confini (multi-)byte dipendenti dall’architettura, come si trova nei linguaggi di programmazione tradizionali. Il programmatore è libero di selezionare qualsiasi dimensione del vettore trit ed il codice generato rifletterà esattamente questa dimensione. Naturalmente è sempre possibile per un programmatore selezionare le dimensioni del vettore trit che approssimano i soliti tipi di dati (multi-)byte dimensionati quando si punta ad un hardware specifico, ma ciò richiede una conoscenza approfondita di come i vettori trit saranno mappati su quello specifico hardware.

Limitare le dimensioni del tipo di dati per far corrispondere gli intervalli di valori effettivamente utilizzati consente di solito di ottenere una riduzione del fabbisogno energetico limitando la quantità di circuiti effettivamente necessari sugli FPGA. Immaginate una variabile che deve solo essere in grado di rappresentare i valori 0-10. Con la maggior parte dei linguaggi di programmazione tradizionali è necessario, come minimo, utilizzare una variabile a 8-bit (da 0 a 255) byte, dove 4 bit (da 0 a 15) sarebbero bastati, ed in ogni caso l’hardware è progettato per manipolare i byte. Con Abra invece può bastare un vettore trit di 3 trits (da 0 a 26) e alla fine su FPGA si ha solo la circuiteria necessaria per manipolare 3 trits.

Trasformazione tramite tabelle di ricerca (look-up table)

Nel cuore di Abra c’è un tipo molto particolare di costruzione statica globale chiamata “look-up table” (LUT). Una LUT è ciò che viene utilizzato per trasformare i valori dei dati in ingresso in valori in uscita.

È possibile vedere una LUT come un’istruzione molto generale, che può essere programmata per eseguire tutti i tipi di operazioni. Per questo motivo, non c’è bisogno di un insieme più complesso di istruzioni predefinite, presente nella maggior parte dei linguaggi di assemblaggio o di livello intermedio. Una LUT può emulare direttamente i risultati di tali istruzioni, oppure è possibile utilizzare più LUT combinati con della logica per creare un effetto equivalente. E le LUT hanno un vantaggio che la maggior parte delle istruzioni “normali” non hanno: possono partecipare al flusso di dati. Questo significa che non hanno bisogno di un segnale di clock esterno per essere in grado di funzionare. Questa è la chiave di un linguaggio che supporta il wave-pipelining. L’unico ‘clock tick’ è fornito dal Supervisore quando fornisce i dati di ingresso da elaborare.

Si noti che ancora una volta, proprio come con i vettori trit, spetta all’implementazione o al programmatore definire l’esatto significato delle LUTs e la specifica Abra è completamente agnostica. Tutto ciò che fa è specificare come i dati passino attraverso una LUT e come i dati si trasformino per calcolare i risultati in modo da mappare su qualsiasi hardware.

Le LUT in Abra possono richiedere fino a 3 trit di input. Il programmatore può specificare esplicitamente per ogni combinazione unica di valori di trit in ingresso quale singolo valore di trit in uscita verrà generato. Per ogni combinazione non specificata di valori di input, o quando un qualsiasi input trit è null, la LUT restituirà null. Ricordiamo che null è l’assenza di flusso di dati. L’importanza di questo aspetto sarà discussa nella sezione sulle operazioni di fusione. Ma prima esamineremo l’anatomia del codice Abra.

Il layout dell’unità di codifica Abra

Un’unità di codice Abra consiste in una sequenza di blocchi che viene divisa in 3 sottosequenze:

  1. Una sequenza di blocchi di definizione LUT. Un blocco di definizione LUT contiene 27 voci, una per ciascuna delle combinazioni uniche possibili di 3 trits di input. Ogni voce specifica il valore trit di uscita per la corrispondente combinazione di ingressi, o un indicatore di null per combinazioni non definite. Quindi, per essere chiari, ognuna delle 27 voci del blocco di definizione LUT può assumere 4 diversi valori che indicano come l’implementazione deve mappare quella combinazione di ingressi su un trit di uscita o su un trit di uscita null
  2. Una sequenza di blocchi di definizione dei rami. Un blocco di definizione del ramo è una sequenza di siti (istruzioni) che formano una singola unità di esecuzione chiamata ramo. I rami sono discussi nella prossima sezione
  3. Una sequenza di blocchi di riferimento esterni. I blocchi di riferimento esterni si riferiscono a blocchi in altre unità di codice Abra. Ogni blocco di riferimento esterno specifica l’hash della specifica unità di codice Abra ed una sequenza di indici dei blocchi in quella unità di codice a cui si riferisce. In questo modo Abra può avere unità di libreria che contengono codice riutilizzabile

Un’unità di codice Abra è completamente autonoma e nel contesto di Qubic sarà memorizzata come un singolo blocco dati codificato in ternario che può essere inviato come payload in un messaggio IOTA. L’unità di codice può essere identificata univocamente dal suo valore hash. Questo rende impossibile manomettere le unità di codice una volta che sono state memorizzate pubblicamente. Un’unità di codice Abra e qualsiasi altra unità di codice a cui si fa riferimento sarà sempre un insieme autoconsistente, a partire dall’hash dell’unità ‘principale’.

Anatomia di un ramo

Un ramo è simile ad una funzione nella maggior parte dei linguaggi di programmazione. Ma, come si vedrà, ci sono anche alcune importanti differenze.

Un ramo è una sequenza di istruzioni Abra chiamate siti. I siti descrivono gli output delle istruzioni Abra associate. Sono simili a variabili locali temporanee ma non occupano spazio di archiviazione. Sarebbe meglio vederli come etichette che fanno riferimento all’output delle istruzioni associate. I siti possono essere utilizzati come input per una o più istruzioni di un altro sito. Sulle implementazioni FPGA i siti saranno cablati direttamente come input ad uno o più siti. Un sito può utilizzare solo siti che hanno già generato un valore come input (flusso di dati). La direzione del grafico così creato è sempre verso avanti, nella direzione del flusso di dati. I siti sono referenziati dal loro indice nella sequenza di siti del ramo.

Un ramo prende sempre come input un unico vettore trit a dimensione fissa e restituisce sempre come output un unico vettore trit a dimensione fissa. Ciò che questi vettori trit rappresentano esattamente dipende come al solito dall’implementazione o programmatore. Anche in questo caso, la specifica Abra è completamente agnostica, perché specifica solo il flusso di dati.

I siti di un ramo sono raggruppati in 4 sottosequenze:

  1. Siti di input. Essi definiscono come il vettore trit di input del ramo sarà suddiviso in sotto-vettori. Infatti, non sono altro che una sequenza di definizioni di dimensioni. Ogni sito di input inizia dove il precedente ha lasciato all’interno del vettore trit di input del ramo. È possibile visualizzare il vettore trit di input come una concatenazione di tutti i sotto-vettori come definito dai siti di input. Si noti che quando il vettore trit di ingresso effettivo è troppo grande, il resto inutilizzato verrà ignorato. E quando il vettore trit di input effettivo è troppo piccolo, Abra assumerà che il resto dei trits necessari sia null. Questo permette di avere alcuni comportamenti interessanti che discuteremo più avanti.
  2. Siti del corpo. Essi definiscono le operazioni intermedie necessarie al ramo per svolgere la sua funzione. Ogni sito è associato ad un’istruzione nodo o fusione. Il vettore trit di input a queste istruzioni è specificato da una sequenza di indici di sito. L’interpretazione di questa sequenza dipende dal tipo di istruzione.
  3. Siti di uscita. Questi sono esattamente gli stessi dei siti del corpo, tranne che definiscono le operazioni finali per completare la funzione del ramo. I risultati di ogni sito di uscita saranno concatenati in sequenza per formare il vettore di uscita del ramo.
  4. Siti bistabili di memoria. Anche questi sono esattamente gli stessi dei siti del corpo, ma svolgono una speciale funzione aggiuntiva. Sono un modo per un ramo di memorizzare lo stato tra un’invocazione e l’altra. Sono definiti per ultimi, perché i bistabili di memoria saranno aggiornati solo dopo che il ramo ha svolto la sua funzione. Ciò significa che, durante l’esecuzione della funzione ramo, ogni volta che un sito bistabile di memoria è referenziato il valore che il bistabile di memoria mantenuto al momento in cui la funzione è stata invocata viene utilizzato. Il valore iniziale di un blocco di memoria è sempre zero.

Ricordiamo che abbiamo detto che la direzione del grafico è sempre in avanti rispetto al flusso di dati? Beh, i siti bistabili di memoria hanno già un valore all’invocazione del ramo, il che significa che è possibile utilizzare questi siti come input ovunque anche se sono definiti solo alla fine. Quindi il flusso di dati è ancora in avanti. E solo dopo che il ramo completa l’esecuzione i valori del sito saranno calcolati e bloccati, il che significa nuovamente che il flusso di dati è in avanti. I siti bistabili di memoria hanno questo modo di essere utilizzati in 2 diversi punti del grafico: una volta all’inizio come ingressi, e una volta alla fine come uscite.

Istruzione nodo di Abra

L’istruzione nodo è giustamente chiamata, perché lega i rami. È simile ad un meccanismo di chiamata di funzione ma con diverse differenze. Un’istruzione nodo prende una sequenza di indici del sito di input e concatena questi siti in un vettore trit di input che viene passato al blocco di destinazione del nodo, che è specificato da un indice nella sequenza di blocchi dell’unità di codice. Ci sono solo due tipi di blocchi a cui il vettore trit concatenato può essere passato:

  1. Un blocco LUT, nel qual caso la dimensione del vettore trit in ingresso deve essere di 1, 2 o 3 trits ed il sito nodo restituirà il singolo trit associato a quella specifica combinazione di trits in ingresso per il blocco LUT in questione
  2. Un blocco di diramazione, nel qual caso il vettore del trit in ingresso può essere di qualsiasi dimensione diversa da zero. Il vettore di trit concatenato sarà passato come vettore di trit in ingresso ad una nuova istanza del ramo e il sito nodo restituirà il vettore di trit in uscita del ramo dopo l’invocazione

Si noti che i blocchi esterni sono semplicemente riferimenti indiretti ai blocchi LUT od ai blocchi di diramazione, quindi non è necessaria una reale differenziazione. È possibile utilizzarli in nodi come se si facesse riferimento direttamente ad essi.

Un nodo ramo non è una chiamata di funzione in senso tradizionale, con un call stack ed una singola istanza della funzione che può essere chiamata da qualsiasi luogo. Invece invoca letteralmente una nuova istanza del ramo a cui si riferisce.

Aiuta pensare ad un ramo come ad un blocco circuitale. Può essere collegato ad un solo ingresso specifico e ad una sola uscita specifica. Pertanto, ogni ramo referenziato attraverso un nodo deve essere istanziato come blocco circuitale separato e quindi i siti di input del nodo devono essere cablati ai siti di input del ramo istanziato, ed i siti di output del ramo istanziato devono essere cablati a tutti i circuiti che usano il sito nodo come input.

Questa istanziazione dei rami ha alcune importanti ramificazioni quando il ramo è stateful perché usa i bistabili di memoria. Perché ogni ramo viene fornito con il proprio set separato di bistabili di memoria. Questo significa che per accedere allo stesso stato del ramo è necessario seguire il percorso di istanziazione originale (o percorso ‘call’), che richiede qualche meccanismo speciale che sarà discusso più in dettaglio una volta arrivati alla descrizione del Supervisore Qubic.

L’istruzione di fusione di Abra

L’istruzione di fusione prende uno o più siti di input che devono essere tutti della stessa dimensione. Di tutti questi siti di input solo uno può essere valutato ad un valore non-null. Il sito di unione restituirà il singolo valore del sito di input non-null, o null quando tutti i siti di input stanno valutando a null. Il valore di ritorno è sempre della stessa dimensione di ogni sito di input. Si noti che avere più di un sito di input non-null è considerato un errore di programmazione e causerà l’attivazione di un’eccezione e l’arresto dell’esecuzione del ramo senza un risultato.

Tubo di collegamento a triplo occhio

È possibile visualizzare l’istruzione di fusione come un tubo di collegamento a triplo occhio, dove un certo numero di tubi di ingresso che confluiscono in un unico tubo di uscita. Immaginate che ogni tubo di ingresso sia collegato ad una diversa sorgente di fluido chimico volatile. Finché nessun rubinetto sorgente è aperto (tutti gli ingressi null) nulla uscirà dal tubo di uscita (uscita null). Se viene aperto un solo rubinetto sorgente (un solo ingresso non-null), il corrispondente fluido chimico fluisce senza problemi attraverso il collegamento nel tubo di uscita (uscita non-null, stesso valore). Ma se vengono aperti più rubinetti sorgente (ingressi multipli non-null), i loro prodotti chimici si mescoleranno nella fusione e causeranno un’esplosione (eccezione).

L’istruzione di fusione eseguirà una funzione chiave nella creazione della logica decisionale. Essenzialmente è possibile impostare più percorsi di esecuzione paralleli (flussi di dati) in un ramo. I risultati di questi percorsi finiscono come diversi siti di input nell’istruzione di fusione, dove solo il risultato desiderato ‘sopravvive’. Poiché è essenziale che al massimo un percorso di esecuzione valuti un valore non-null, è necessario un modo per introdurre questi valori null. È qui che le LUT sono utili. In ogni percorso di esecuzione una LUT può essere usata come filtro che permette il flusso di dati solo quando sono soddisfatte condizioni specifiche. È sufficiente fare attenzione che le condizioni in cui le LUT sono filtrate siano reciprocamente escludenti in ogni percorso di esecuzione.

A proposito, questo è l’unico meccanismo che ci permette di prendere una decisione in Abra. Abra non ha nessuno dei classici comandi di controllo del flusso. Ciò significa che non ci sono dichiarazioni di decisione come if-then-else in sé, dove viene preso solo un singolo percorso di esecuzione a seconda di una condizione e l’altra viene saltata, né ci sono dichiarazioni di looping come for-loops e while-loops. Tali costrutti impedirebbero il flusso dei dati. L’assenza di questi costrutti rende inoltre facile mantenere pure le funzioni e ci permette di ragionare sulla loro correttezza quando la si guarda dal punto di vista di una singola invocazione di un ramo.

Anche con la limitata quantità di meccanismi finora descritti, è ancora possibile creare un costrutto di looping. È possibile impostare un ramo che ha due percorsi di esecuzione, uno filtrato sulla condizione di continuazione del ciclo e uno sulla condizione di fine ciclo. Il primo percorso chiamerebbe un ramo che esegue un’iterazione e poi chiama la funzione loop ricorsivamente. Il secondo percorso chiamerebbe una funzione di continuazione che riprende l’esecuzione dopo che il ciclo è stato fatto.

Naturalmente la ricorsione non è sempre la soluzione migliore, specialmente di fronte al fatto che i rami vengono istanziati ogni volta che vengono chiamati. Per poter istanziare un ramo, tutto ciò che viene chiamato da quel ramo deve essere istanziato. Questo potrebbe diventare, abbastanza rapidamente, una quantità proibitiva di circuiti, a seconda della profondità della ricorsione. Per questo motivo, e anche per essere sicuri che sia impossibile entrare in un ciclo infinito, Abra richiede di specificare una profondità massima di invocazioni di ramo per un’unità di codice. Quando questo limite di profondità è raggiunto, invece di invocare un altro ramo Abra ritornerà null per quell’invocazione, così il flusso di dati si fermerà.

Finora abbiamo guardato al codice Abra solo dal punto di vista di una singola invocazione di un ramo. Ma naturalmente è possibile invocare un ramo più volte. Altrimenti non avrebbe molto senso avere la possibilità di memorizzare lo stato nelle bistabili di memoria. Questo ha senso solo a fronte di invocazioni multiple. Quindi è qui che inizieremo ad introdurre concetti che hanno lo scopo di facilitare l’interazione con il Supervisore Qubic.

Flusso dati reattivo in Abra

Il flusso di dati in Abra è reattivo. Ciò significa che le entità computazionali rispondono a cambiamenti discreti nei dati di input ricalcolando gli output di tutte le entità che dipendono da questi dati di input. Noi chiamiamo tale evento quando i dati di input cambiano un effetto. Un effetto è sempre inviato direttamente ad un ambiente. Un effetto può essere elaborato da qualsiasi entità computazionale. Affinché un effetto possa essere processato da un’entità, questa entità deve prima unirsi all’ambiente corrispondente. Unendosi ad un ambiente, l’entità indica che vuole essere notificata ogni volta che gli effetti si verificano in quell’ambiente, in modo da poter elaborare i dati degli effetti. Chiamiamo questo meccanismo AEE (per Ambiente, Entità, Effetto) e regola come entità separate interagiscono tra loro.

In Abra, è possibile designare qualsiasi ramo come entità e farla entrare in uno o più ambienti. Un effetto che arriva a tale ambiente unito sarà inviato come dati di input all’entità dal Supervisore. I dati dell’effetto fluiranno poi attraverso l’entità fino alla produzione di un risultato di output. Tale flusso singolo di dati attraverso l’entità viene chiamato onda. Un’onda è quindi una singola iterazione attraverso il codice che inizia passando i dati dell’effetto all’entità e termina quando un risultato di output viene prodotto dall’entità.

Quello che è importante notare è che un’onda è un’unità atomica di elaborazione. Questo significa che con Abra non usiamo una programmazione preventiva dei compiti con il relativo contesto di commutazione sopraelevato. La fine di un’onda forma un punto di commutazione del contesto naturale. La natura atomica di un’onda significa anche che c’è un bisogno molto ridotto di sincronizzazione perché non è necessario rendere conto di tutto ciò che potrebbe interrompere un’onda.

I dati dei risultati di output prodotti da un’entità possono essere inviati come effetto ad uno o più ambienti per un’ulteriore elaborazione nell’onda successiva (a meno che non sia stato esplicitamente contrassegnato per essere ritardato). Questo significa che un’onda può innescare una cascata di altre onde, che continuerà fino al raggiungimento di una nuova situazione stabile (cioè non ci sono più effetti in coda in attesa da elaborare nel ciclo corrente). Una tale cascata di onde, che è iniziata con l’arrivo di un effetto, è quella che noi chiamiamo un quant. Poiché l’esatta quantità di onde in un quant e ciò che accade durante queste onde dipende dall’implementazione, un quant non ha un periodo di tempo fisso ad esso associato. Ma incarna un insieme completo e coerente di calcoli.

All’interno di un quant, gli unici effetti che possono innescare una nuova onda sono gli effetti generati dai calcoli all’interno del quant stesso. Eventuali effetti esterni che si verificano durante l’elaborazione avviene all’interno di un quant sono posticipati fino all’inizio del quant successivo. Inoltre, gli effetti interni possono essere ritardati di proposito da un numero qualsiasi di quant.

Si noti che Abra previene una cascata infinita di onde richiedendo alle entità di specificare un limite di invocazione. Quando tale limite è stato raggiunto all’interno di un quant, qualsiasi effetto che potrebbe causare il superamento del limite di invocazione sarà posticipato fino all’inizio del quant successivo. Questo garantisce che un quant non possa entrare in un ciclo infinito di invocazione. All’inizio del quant successivo i contatori di invocazione vengono resettati e l’elaborazione degli effetti rinviati può riprendere.

Le applicazioni con componenti time-critical possono utilizzare questo limite di invocazione per ridurre la lunghezza di un quant, oppure possono specificare una quantità specifica di quant per ritardare la ripresa dell’invocazione. Ciò consente ai componenti critici in termini di tempo di servire tempestivamente i loro effetti (esterni).

Si noti che possiamo usare l’AEE per realizzare il looping in Abra facendo sì che l’entità generi un effetto in un ambiente a cui si è unita, il che significa che la stessa entità sarà nuovamente innescata dall’effetto che ha generato. I dati associati all’effetto possono essere utilizzati come dati di input per la prossima iterazione. Lo stato di iterazione è inviato come parte dei dati dell’effetto. Il looping in questo modo è molto più controllato, perché ogni iterazione è fatta in una singola onda. Esso è anche limitato dai limiti di invocazione, il che significa che l’esecuzione di loop con grandi quantità di iterazioni può essere distribuita su quante multiple. Si noti che questo significa anche che i loop infiniti sono sicuramente possibili in questo modo, ma poiché sono ora governati dal Supervisore, non saranno mai in grado di bloccare il sistema.

Si noti inoltre che invocare un’entità tramite AEE significa che essa ricomincia nella parte superiore del percorso di istanziazione del ramo, il che significa che attraverso un’attenta programmazione è possibile assicurarsi di finire in un ramo che precedentemente memorizzato in blocchi di memoria ed elaborare l’effetto nel contesto di quello stato.

Le interazioni tra il Supervisore Qubic, le entità del ramo Abra, altre entità ed il modo in cui gli effetti sono generati e interagiscono con gli ambienti saranno discussi in modo approfondito dopo la discussione su come Qupla implementa Abra. Per facilitare questa discussione prima di tutto si introduce uno pseudo-codice umano leggibile per Abra.

Abra pseudo-codice

È stato ideato uno pseudo-codice per Abra in modo da avere istruzioni leggibili dagli umani. Ogni tipo di blocco nell’unità di codice ha il suo specifico layout.

// 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.

Si noti che non sono ancora stati usati blocchi esterni perché per ora tutto è tirato in un’unica unità di codice, e quanto sopra probabilmente non sarà la notazione finale. È esclusivamente menzionato qui per completezza.

// 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 @[email protected]@[email protected]@[email protected]@[email protected]@10 not_0
lut 100010001100010001100010001 equal_0

La tabella delle uscite LUT è ordinata per combinazione di ingressi da ---, 0--, 0--, 1--, 1--, a -11, 011, 111 (quindi contando da -13 a +13, con codifica ternaria bilanciata big-endian). Si noti che poiché si hanno sempre 27 valori nella tabella, 1 ingresso trit LUTs avrà i loro 3 valori replicati 9 volte, e 2 ingressi trit LUTs avranno i loro 9 valori replicati 3 volte.

// 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 descrizione di cui sopra è deliberatamente mantenuta concisa. Si troverà che le istruzioni di Abra sono così semplici che capire cosa sta succedendo è un gioco da ragazzi.

Conclusioni

La specifica Abra definisce il flusso di dati pur rimanendo largamente agnostico dell’implementazione utilizzando una serie molto limitata di istruzioni per la costruzione dei blocchi. Abra supporta un meccanismo cooperativo multitasking. Questo meccanismo funziona in collaborazione con il Supervisore Qubic che sarà discusso in un futuro articolo. È stata descritta una notazione pseudo-codice per le istruzioni di Abra che aiuterà ad avere un’idea della specifica di Abra stessa in modo che tutte le parti si uniscano. Nella prossima parte di questa serie sarà approfondito il linguaggio di programmazione Qupla, che implementa le specifiche Abra.


Il testo originale in lingua inglese si trova qui: https://blog.iota.org/explaining-the-qubic-computation-model-part-1-13c9e5820ed


Per ulteriori informazioni in italiano o tedesco trovate i miei contatti a questa pagina.
Se avete trovato utile la mia libera traduzione, accetto volentieri delle donazioni 😉

IOTA:
QOQJDKYIZYKWASNNILZHDCTWDM9RZXZV9DUJFDRFWKRYPRMTYPEXDIVMHVRCNXRSBIJCJYMJ9EZ9USHHWKEVEOSOZB
BTC:
1BFgqtMC2nfRxPRge5Db3gkYK7kDwWRF79

Non garantisco nulla e mi libero da ogni responsabilità.