Costruisci un gioco funzionante di Tetris in Conway's Game of Life


994

Ecco una domanda teorica - una che non offre una risposta facile in ogni caso, nemmeno quella banale.

Nel gioco della vita di Conway esistono costrutti come il metapixel che consentono al gioco della vita di simulare anche qualsiasi altro sistema di regole del gioco della vita. Inoltre, è noto che il gioco della vita è Turing completo.

Il tuo compito è quello di costruire un automa cellulare usando le regole del gioco della vita di Conway che permetteranno di giocare a un gioco di Tetris.

Il programma riceverà input modificando manualmente lo stato dell'automa di una generazione specifica per rappresentare un interrupt (ad esempio, spostando un pezzo a sinistra o a destra, facendolo cadere, ruotandolo o generando casualmente un nuovo pezzo da posizionare sulla griglia), contando un numero specifico di generazioni come tempo di attesa e visualizzazione del risultato da qualche parte sull'automa. Il risultato visualizzato deve assomigliare visibilmente a una griglia Tetris effettiva.

Il tuo programma verrà valutato in base alle seguenti cose, in ordine (con criteri inferiori che fungono da tiebreak per criteri più alti):

  • Dimensioni del riquadro di delimitazione: vince il riquadro rettangolare con l'area più piccola che contiene completamente la soluzione data.

  • Piccole modifiche all'input: il numero minimo di celle (nel caso peggiore dell'automa) che deve essere regolato manualmente per un interruzione vince.

  • Esecuzione più rapida: vince il minor numero di generazioni per avanzare di un segno di spunta nella simulazione.

  • Conteggio iniziale delle cellule vive - vince un conteggio inferiore.

  • Prima di postare - vince post precedente.


95
"Esempio funzionante in modo dimostrabile" significa qualcosa che funziona nel giro di poche ore, o qualcosa che può essere dimostrato corretto anche se ci vorrebbe fino alla morte del caldo dell'universo per giocare?
Peter Taylor,

34
Sono abbastanza sicuro che qualcosa del genere sia possibile e giocabile. È solo che pochissime persone hanno le competenze per poter programmare quello che è probabilmente uno dei "linguaggi di assemblaggio" più esoterici al mondo.
Justin L.,

58
Questa sfida è in fase di elaborazione! Chat room | Progresso | Blog
mbomb007,

49
Alle 5:10 di questa mattina (9:10 UTC), questa domanda è la prima domanda nella storia di PPCG a raggiungere 100 voti senza ottenere una risposta! Complimenti a tutti.
Joe Z.

76
Sto cercando di risolvere questo ... Ora, quando vado a letto, vedo alianti dappertutto, che si scontrano in un disastro gigantesco. I miei sogni sono pieni di incubi in cui pentadecathlon pulsanti mi bloccano la strada e Herschels si sta evolvendo per assorbirmi. Per favore, John Conway, prega per me ...
dim

Risposte:


938

Questo è iniziato come una ricerca ma è finito come un'odissea.

Quest for Tetris Processor, 2.940.928 x 10.295.296

Il file di pattern, in tutto il suo splendore, può essere trovato qui , visualizzabile nel browser qui .

Questo progetto è il culmine degli sforzi di molti utenti nel corso degli ultimi 1 e 1/2 anni. Sebbene la composizione della squadra sia variata nel tempo, i partecipanti al momento della scrittura sono i seguenti:

Vorremmo anche estendere i nostri ringraziamenti a 7H3_H4CK3R, Conor O'Brien e ai molti altri utenti che si sono impegnati a risolvere questa sfida.

A causa della portata senza precedenti di questa collaborazione, questa risposta è suddivisa in parti su più risposte scritte dai membri di questo team. Ogni membro scriverà su argomenti specifici, approssimativamente corrispondenti alle aree del progetto in cui sono stati maggiormente coinvolti.

Distribuire eventuali voti positivi o taglie a tutti i membri del team.

Sommario

  1. Panoramica
  2. Metapixels e VarLife
  3. Hardware
  4. QFTASM e Cogol
  5. Assemblea, traduzione e futuro
  6. Nuova lingua e compilatore

Considera anche di controllare la nostra organizzazione GitHub dove abbiamo inserito tutto il codice che abbiamo scritto come parte della nostra soluzione. Le domande possono essere indirizzate alla nostra chat di sviluppo .


Parte 1: Panoramica

L'idea alla base di questo progetto è l' astrazione . Invece di sviluppare direttamente un gioco Tetris in Life, abbiamo lentamente aumentato l'astrazione in una serie di passaggi. Ad ogni livello, ci allontaniamo dalle difficoltà della vita e ci avviciniamo alla costruzione di un computer facile da programmare come un altro.

Innanzitutto, abbiamo utilizzato i metapixel OTCA come base del nostro computer. Questi metapixel sono in grado di emulare qualsiasi regola "realistica". Wireworld e il computer Wireworld sono stati importanti fonti di ispirazione per questo progetto, quindi abbiamo cercato di creare una struttura simile con i metapixel. Sebbene non sia possibile emulare Wireworld con metapixel OTCA, è possibile assegnare metapixel diversi a regole diverse e creare disposizioni metapixel che funzionano in modo simile ai fili.

Il passo successivo è stato quello di costruire una varietà di porte logiche fondamentali come base per il computer. Già in questa fase abbiamo a che fare con concetti simili al design del processore reale. Ecco un esempio di una porta OR, ogni cella in questa immagine è in realtà un intero metapixel OTCA. Puoi vedere "elettroni" (ognuno dei quali rappresenta un singolo bit di dati) entrare e uscire dal gate. Puoi anche vedere tutti i diversi tipi di metapixel che abbiamo usato nel nostro computer: B / S come sfondo nero, B1 / S in blu, B2 / S in verde e B12 / S1 in rosso.

Immagine

Da qui abbiamo sviluppato un'architettura per il nostro processore. Abbiamo dedicato notevoli sforzi alla progettazione di un'architettura tanto meno esoterica quanto più facilmente implementabile possibile. Mentre il computer Wireworld utilizzava un'architettura rudimentale innescata dal trasporto, questo progetto utilizza un'architettura RISC molto più flessibile completa di più codici operativi e modalità di indirizzamento. Abbiamo creato un linguaggio assembly, noto come QFTASM (Quest for Tetris Assembly), che ha guidato la costruzione del nostro processore.

Il nostro computer è anche asincrono, il che significa che non esiste un orologio globale che controlla il computer. Piuttosto, i dati sono accompagnati da un segnale di orologio mentre scorre attorno al computer, il che significa che dobbiamo concentrarci solo sui tempi locali ma non globali del computer.

Ecco un'illustrazione della nostra architettura di processore:

Immagine

Da qui si tratta solo di implementare Tetris sul computer. Per raggiungere questo obiettivo, abbiamo lavorato su più metodi di compilazione di un linguaggio di livello superiore in QFTASM. Abbiamo un linguaggio di base chiamato Cogol, un secondo linguaggio più avanzato in fase di sviluppo, e infine abbiamo un back-end GCC in costruzione. L'attuale programma Tetris è stato scritto / compilato da Cogol.

Una volta generato il codice QFTASM finale di Tetris, i passaggi finali sono stati l'assemblaggio da questo codice alla ROM corrispondente, quindi dai metapixel al gioco della vita sottostante, completando la nostra costruzione.

Esecuzione di Tetris

Per coloro che desiderano giocare a Tetris senza scherzare con il computer, è possibile eseguire il codice sorgente di Tetris sull'interprete QFTASM . Impostare gli indirizzi di visualizzazione RAM su 3-32 per visualizzare l'intero gioco. Ecco un permalink per comodità: Tetris in QFTASM .

Caratteristiche del gioco:

  • Tutti e 7 i tetromini
  • Movimento, rotazione, gocce morbide
  • Cancella riga e punteggio
  • Anteprima
  • Gli input del giocatore iniettano casualità

Schermo

Il nostro computer rappresenta la scheda Tetris come una griglia nella sua memoria. Gli indirizzi 10-31 mostrano il tabellone, gli indirizzi 5-8 mostrano l'anteprima e l'indirizzo 3 contiene il punteggio.

Ingresso

L'immissione nel gioco viene eseguita modificando manualmente il contenuto dell'indirizzo RAM 1. Utilizzando l'interprete QFTASM, ciò significa eseguire scritture dirette all'indirizzo 1. Cercare "Scrittura diretta su RAM" nella pagina dell'interprete. Ogni spostamento richiede solo la modifica di un singolo bit di RAM e questo registro di input viene automaticamente cancellato dopo la lettura dell'evento di input.

value     motion
   1      counterclockwise rotation
   2      left
   4      down (soft drop)
   8      right
  16      clockwise rotation

Sistema di punteggio

Ottieni un bonus per aver eliminato più righe in un solo turno.

1 row    =  1 point
2 rows   =  2 points
3 rows   =  4 points
4 rows   =  8 points

14
@ Christopher2EZ4RTZ Questo post di sintesi descrive in dettaglio il lavoro svolto da molti dei membri del progetto (compresa la scrittura effettiva del post di panoramica). Come tale, è appropriato che sia CW. Stavamo anche cercando di evitare che una persona avesse due post, perché ciò avrebbe causato loro un ingiusto numero di rappresentanti, dal momento che stiamo cercando di mantenere uniforme il rappresentante.
Mego

28
Prima di tutto +1, perché questo è un risultato follemente fantastico (soprattutto da quando hai costruito un computer nel gioco della vita, piuttosto che solo tetris). In secondo luogo, quanto è veloce il computer e quanto è veloce il gioco tetris? È anche giocabile in remoto? (di nuovo: è fantastico)
Socratic Phoenix

18
Questo ... questo è completamente folle. +1 a tutte le risposte immediatamente.
scottinet,

28
Un avvertimento per chiunque desideri distribuire piccoli doni sulle risposte: devi raddoppiare il tuo importo di ricompensa ogni volta (fino a quando non colpisci 500), quindi una sola persona non può dare lo stesso importo a ogni risposta a meno che tale importo non sia di 500 rep.
Martin Ender,

23
Questa è la cosa più grande che abbia mai sfogliato pur comprendendo ben poco.
Ingegnere Toast,

678

Parte 2: OTCA Metapixel e VarLife

Metapixel OTCA

Metapixel OTCA
( Fonte )

Il metapixel OTCA è un costrutto nel gioco della vita di Conway che può essere utilizzato per simulare qualsiasi automa cellulare simile alla vita. Come dice LifeWiki (linkato sopra),

Il metapixel OTCA è una cellula unitaria 35328 del periodo 2048 × 2048 costruita da Brice Due ... Ha molti vantaggi ... tra cui la capacità di emulare qualsiasi automa cellulare simile alla vita e il fatto che, quando viene ingrandito, l'ON e le celle OFF sono facili da distinguere ...

Ciò che significa automi cellulari simili alla vita qui è essenzialmente che le cellule nascono e le cellule sopravvivono in base a quante delle loro otto cellule vicine sono vive. La sintassi di queste regole è la seguente: una B seguita dal numero di vicini vivi che causeranno un parto, quindi una barra, quindi una S seguita dal numero di vicini vivi che manterranno viva la cellula. Un po 'prolisso, quindi penso che un esempio possa essere d'aiuto. Il gioco canonico della vita può essere rappresentato dalla regola B3 / S23, che dice che qualsiasi cellula morta con tre vicini vivi diventerà viva e qualsiasi cellula viva con due o tre vicini vivi rimarrà viva. Altrimenti, la cellula muore.

Nonostante sia una cella 2048 x 2048, il metapixel OTCA in realtà ha un riquadro di delimitazione di 2058 x 2058 celle, il motivo è che si sovrappone di cinque celle in ogni direzione con i suoi vicini diagonali . Le celle sovrapposte servono per intercettare gli alianti - che sono emessi per segnalare ai vicini metacellette che è acceso - in modo che non interferiscano con altri metapixel o volino via indefinitamente. Le regole di nascita e sopravvivenza sono codificate in una sezione speciale di cellule sul lato sinistro del metapixel, dalla presenza o assenza di mangiatori in posizioni specifiche lungo due colonne (una per la nascita, l'altra per la sopravvivenza). Per quanto riguarda il rilevamento dello stato delle cellule vicine, ecco come succede:

Un flusso 9-LWSS gira quindi in senso orario attorno alla cellula, perdendo un LWSS per ogni cellula "on" adiacente che ha innescato una reazione al miele. Il numero di LWSS mancanti viene contato rilevando la posizione dell'LWSS anteriore facendo schiantare un altro LWSS dalla direzione opposta. Questa collisione rilascia alianti, che innesca un'altra o due reazioni al miele se i mangiatori che indicano che le condizioni di nascita / sopravvivenza sono assenti.

Un diagramma più dettagliato di ciascun aspetto del metapixel OTCA è disponibile sul sito Web originale: come funziona? .

VarLife

Ho creato un simulatore online di regole simil-vita in cui è possibile far comportare qualsiasi cellula in base a qualsiasi regola realistica e l'ho chiamato "Variazioni della vita". Questo nome è stato abbreviato in "VarLife" per essere più conciso. Ecco uno screenshot di esso (link qui: http://play.starmaninnovations.com/varlife/BeeHkfCpNR ):

Screenshot di VarLife

Caratteristiche notevoli:

  • Commuta le celle tra vivo / morto e dipingi la scheda con regole diverse.
  • La possibilità di avviare e interrompere la simulazione e di fare un passo alla volta. È anche possibile eseguire un determinato numero di passaggi il più rapidamente possibile o più lentamente, alla velocità impostata nelle caselle tick al secondo e millisecondi al tick.
  • Cancella tutte le celle vive o per ripristinare completamente la scheda su uno stato vuoto.
  • Può cambiare le dimensioni della cella e della scheda e anche per consentire l'avvolgimento toroidale in orizzontale e / o in verticale.
  • Permalink (che codificano tutte le informazioni nell'URL) e URL brevi (perché a volte ci sono troppe informazioni, ma sono comunque belle).
  • Set di regole, con specifiche B / S, colori e casualità opzionale.
  • E ultimo ma sicuramente non meno importante, il rendering di gif!

La funzione render-to-gif è la mia preferita sia perché ci è voluto un sacco di lavoro da implementare, quindi è stato davvero soddisfacente quando l'ho finalmente rotto alle 7 del mattino, e perché rende molto facile condividere i costrutti VarLife con gli altri .

Circuito di base VarLife

Tutto sommato, il computer VarLife necessita solo di quattro tipi di celle! Otto stati in tutto contando gli stati morti / vivi. Loro sono:

  • B / S (bianco / nero), che funge da buffer tra tutti i componenti poiché le celle B / S non possono mai essere vive.
  • B1 / S (blu / ciano), che è il tipo di cella principale utilizzato per propagare i segnali.
  • B2 / S (verde / giallo), utilizzato principalmente per il controllo del segnale, assicurando che non si ripropaghi.
  • B12 / S1 (rosso / arancione), utilizzato in alcune situazioni specializzate, come l'attraversamento di segnali e la memorizzazione di un po 'di dati.

Utilizzare questo breve URL per aprire VarLife con queste regole già codificate: http://play.starmaninnovations.com/varlife/BeeHkfCpNR .

fili

Esistono diversi design di filo con caratteristiche diverse.

Questo è il filo più semplice e basilare di VarLife, una striscia di blu delimitata da strisce di verde.

filo di base
Breve URL: http://play.starmaninnovations.com/varlife/WcsGmjLiBF

Questo filo è unidirezionale. Cioè, ucciderà qualsiasi segnale che tenta di viaggiare nella direzione opposta. È anche una cella più stretta del filo di base.

filo unidirezionale
Breve URL: http://play.starmaninnovations.com/varlife/ARWgUgPTEJ

Esistono anche fili diagonali ma non sono usati molto.

filo diagonale
Breve URL: http://play.starmaninnovations.com/varlife/kJotsdSXIj

Gates

In realtà ci sono molti modi per costruire ogni singolo cancello, quindi mostrerò solo un esempio per ogni tipo. Questa prima gif mostra le porte AND, XOR e OR, rispettivamente. L'idea di base qui è che una cellula verde si comporta come un AND, una cellula blu si comporta come un XOR e una cellula rossa si comporta come un OR, e tutte le altre celle intorno a loro sono proprio lì per controllare correttamente il flusso.

Porte logiche AND, XOR, OR
Breve URL: http://play.starmaninnovations.com/varlife/EGTlKktmeI

La porta AND-NOT, abbreviata in "porta ANT", si è rivelata un componente vitale. È un gate che passa un segnale da A se e solo se non c'è segnale da B. Quindi, "A AND NOT B".

Porta AND-NOT
Breve URL: http://play.starmaninnovations.com/varlife/RsZBiNqIUy

Sebbene non sia esattamente un cancello , una piastrella che attraversa il filo è ancora molto importante e utile da avere.

attraversamento del filo
Breve URL: http://play.starmaninnovations.com/varlife/OXMsPyaNTC

Per inciso, qui non c'è un cancello NOT. Questo perché senza un segnale in ingresso, deve essere prodotto un output costante, che non funziona bene con la varietà di temporizzazioni richieste dall'hardware del computer corrente. Andavamo d'accordo senza di essa comunque.

Inoltre, molti componenti sono stati progettati intenzionalmente per adattarsi all'interno di un riquadro di delimitazione 11 per 11 (una tessera ) in cui sono necessari 11 tick per entrare nella tessera per uscire dalla tessera. Ciò rende i componenti più modulari e più facili da schiaffeggiare secondo necessità, senza doversi preoccupare di regolare i cavi per la spaziatura o la temporizzazione.

Per vedere più gate che sono stati scoperti / costruiti durante l'esplorazione dei componenti dei circuiti, dai un'occhiata a questo post sul blog di PhiNotPi: Building Blocks: Logic Gates .

Componenti di ritardo

Nel processo di progettazione dell'hardware del computer, KZhang ha ideato più componenti di ritardo, mostrate di seguito.

Ritardo di 4 tick: URL breve: http://play.starmaninnovations.com/varlife/gebOMIXxdh
Ritardo di 4 tick

Ritardo di 5 tick: URL breve: http://play.starmaninnovations.com/varlife/JItNjJvnUB
Ritardo di 5 tick

Ritardo di 8 tick (tre diversi punti di ingresso): URL breve: http://play.starmaninnovations.com/varlife/nSTRaVEDvA
8 ritardo di tick

Ritardo di 11 tick: URL breve: http://play.starmaninnovations.com/varlife/kfoADussXA
11 ritardo di tick

Ritardo di 12 tick: URL breve: http://play.starmaninnovations.com/varlife/bkamAfUfud
12 ritardo di tick

Ritardo di 14 tick: URL breve: http://play.starmaninnovations.com/varlife/TkwzYIBWln
14 ritardo di tick

Ritardo di 15 tick (verificato confrontandolo con questo ): URL breve: http://play.starmaninnovations.com/varlife/jmgpehYlpT
Ritardo di 15 tick

Bene, questo è tutto per i componenti di base dei circuiti in VarLife! Vedi il post hardware di KZhang per i principali circuiti del computer!


4
VarLife è una delle parti più impressionanti di questo progetto; è versatilità e semplicità rispetto, ad esempio, a Wireworld è fenomenale. Il Metapixel OTCA sembra essere molto più grande del necessario, ci sono stati tentativi di abbatterlo?
primo


6
Sì, questo fine settimana ha fatto un discreto progresso nel cuore di un metacell compatibile con HashLife 512x512 ( conwaylife.com/forums/viewtopic.php?f=&p=51287#p51287 ). Il metacell potrebbe essere leggermente ridimensionato, a seconda della dimensione di un'area "pixel" che segnala lo stato della cella quando si esegue lo zoom verso l'esterno. Sicuramente vale la pena fermarsi su un riquadro esatto di dimensioni 2 ^ N, tuttavia, poiché l'algoritmo HashLife di Golly sarà in grado di eseguire il computer molto più velocemente.
Dave Greene,

2
I cavi e le porte non possono essere implementati in modo meno "dispendioso"? Un elettrone sarebbe rappresentato da un aliante o un'astronave (a seconda della direzione). Ho visto accordi che li reindirizzano (e cambiano da uno all'altro se necessario) e alcuni cancelli che lavorano con gli alianti. Sì, occupano più spazio, il design è più complicato e i tempi devono essere precisi. Ma una volta che hai quei blocchi di base, dovrebbero essere abbastanza facili da mettere insieme e occuperebbero molto meno spazio di VarLife implementato usando OTCA. Sarebbe anche più veloce.
Heimdall,

@Heimdall Anche se funzionerebbe bene, non si mostrerebbe bene durante la riproduzione di Tetris.
MilkyWay90,

649

Parte 3: Hardware

Con la nostra conoscenza delle porte logiche e della struttura generale del processore, possiamo iniziare a progettare tutti i componenti del computer.

demultiplexer

Un demultiplexer, o demux, è un componente cruciale per ROM, RAM e ALU. Indirizza un segnale di ingresso a uno dei numerosi segnali di uscita in base ad alcuni dati di selezione dati. È composto da 3 parti principali: un convertitore da seriale a parallelo, un controllore di segnale e uno splitter di segnale dell'orologio.

Iniziamo convertendo i dati del selettore seriale in "parallelo". Questo viene fatto dividendo e ritardando strategicamente i dati in modo che il bit di dati più a sinistra intersechi il segnale di clock nel riquadro 11x11 più a sinistra, il bit di dati successivo intersechi il segnale di clock nel quadrato 11x11 successivo e così via. Sebbene ogni bit di dati verrà emesso in ogni quadrato 11x11, ogni bit di dati si intersecherà con il segnale di clock solo una volta.

Convertitore da seriale a parallelo

Successivamente, verificheremo se i dati paralleli corrispondono a un indirizzo preimpostato. Lo facciamo utilizzando porte AND e ANT sull'orologio e dati paralleli. Tuttavia, dobbiamo assicurarci che anche i dati paralleli vengano emessi in modo che possano essere nuovamente confrontati. Queste sono le porte che mi sono inventato:

Cancelli di controllo del segnale

Infine, abbiamo appena diviso il segnale di clock, impilato un sacco di controllori di segnale (uno per ogni indirizzo / uscita) e abbiamo un multiplexer!

multiplexer

rom

La ROM dovrebbe prendere un indirizzo come input e inviare le istruzioni a quell'indirizzo come output. Iniziamo utilizzando un multiplexer per indirizzare il segnale di clock a una delle istruzioni. Successivamente, abbiamo bisogno di generare un segnale utilizzando alcuni incroci di filo e porte OR. Gli incroci di filo consentono al segnale di clock di spostarsi verso il basso su tutti i 58 bit dell'istruzione e consentono anche che un segnale generato (attualmente in parallelo) si sposti verso il basso attraverso la ROM per essere emesso.

Bit ROM

Quindi non ci resta che convertire il segnale parallelo in dati seriali e la ROM è completa.

Convertitore da parallelo a seriale

rom

La ROM è attualmente generata eseguendo uno script in Golly che tradurrà il codice assembly dagli Appunti in ROM.

SRL, SL, SRA

Queste tre porte logiche vengono utilizzate per i bit shift e sono più complicate dei normali AND, OR, XOR, ecc. Per far funzionare queste porte, ritarderemo prima il segnale dell'orologio per un periodo di tempo appropriato per causare uno "spostamento" nei dati. Il secondo argomento dato a queste porte determina quanti bit spostare.

Per SL e SRL, dobbiamo

  1. Assicurarsi che i 12 bit più significativi non siano attivi (altrimenti l'uscita è semplicemente 0) e
  2. Ritardare la quantità corretta di dati in base ai 4 bit meno significativi.

Questo è fattibile con un gruppo di porte AND / ANT e un multiplexer.

SRL

L'SRA è leggermente diverso, perché durante il turno dobbiamo copiare il bit del segno. Facciamo questo ANDando il segnale di clock con il bit di segno, e quindi copiando quell'output un sacco di volte con splitter di fili e porte OR.

SRA

Latch set-reset (SR)

Molte parti della funzionalità del processore si basano sulla capacità di memorizzare i dati. Usando 2 celle rosse B12 / S1, possiamo fare proprio questo. Le due celle possono tenersi reciprocamente attive e possono anche stare insieme. Usando alcuni circuiti extra di set, reset e read, possiamo fare un semplice latch SR.

Scrocco SR

Synchronizer

Convertendo i dati seriali in dati paralleli, quindi impostando un gruppo di blocchi SR, è possibile memorizzare un'intera parola di dati. Quindi, per ottenere nuovamente i dati, possiamo semplicemente leggere e ripristinare tutti i blocchi e ritardare i dati di conseguenza. Questo ci consente di memorizzare una (o più) parole di dati mentre ne attende un'altra, consentendo la sincronizzazione di due parole di dati che arrivano in momenti diversi.

Synchronizer

Leggi contatore

Questo dispositivo tiene traccia di quante altre volte deve rivolgersi dalla RAM. Lo fa usando un dispositivo simile al latch SR: un infradito T. Ogni volta che il Flip-flop T riceve un input, cambia stato: se era acceso, si spegne e viceversa. Quando il Flip-flop T viene capovolto da on a off, invia un impulso di uscita, che può essere inserito in un altro Flip-flop T per formare un contatore a 2 bit.

Contatore a due bit

Per creare il contatore di lettura, è necessario impostare il contatore sulla modalità di indirizzamento appropriata con due porte ANT e utilizzare il segnale di uscita del contatore per decidere dove indirizzare il segnale di clock: verso ALU o RAM.

Leggi contatore

Coda di lettura

La coda di lettura deve tenere traccia di quale contatore di lettura ha inviato un input alla RAM, in modo che possa inviare l'output della RAM nella posizione corretta. Per fare ciò, utilizziamo alcuni latch SR: un latch per ogni input. Quando un segnale viene inviato alla RAM da un contatore di lettura, il segnale di clock viene diviso e imposta il latch SR del contatore. L'uscita della RAM viene quindi ANDed con il latch SR e il segnale di clock dalla RAM reimposta il latch SR.

Coda di lettura

ALU

L'ALU funziona in modo simile alla coda di lettura, in quanto utilizza un latch SR per tenere traccia di dove inviare un segnale. Innanzitutto, il latch SR del circuito logico corrispondente al codice operativo dell'istruzione viene impostato mediante un multiplexer. Successivamente, i valori del primo e del secondo argomento sono ANDed con il latch SR e quindi passati ai circuiti logici. Il segnale dell'orologio reimposta il fermo man mano che passa in modo da poter riutilizzare ALU. (La maggior parte dei circuiti è interrotta e viene inserita una tonnellata di gestione del ritardo, quindi sembra un po 'un casino)

ALU

RAM

La RAM è stata la parte più complicata di questo progetto. Richiedeva un controllo molto specifico su ciascun latch SR che memorizzava i dati. Per la lettura, l'indirizzo viene inviato in un multiplexer e inviato alle unità RAM. Le unità RAM emettono i dati che memorizzano in parallelo, che viene convertito in seriale e trasmesso. Per la scrittura, l'indirizzo viene inviato in un multiplexer diverso, i dati da scrivere vengono convertiti da seriale a parallelo e le unità RAM propagano il segnale attraverso la RAM.

Ogni unità RAM da 22x22 metapixel ha questa struttura di base:

Unità RAM

Mettendo insieme l'intera RAM, otteniamo qualcosa che assomiglia a questo:

RAM

Mettere tutto insieme

Utilizzando tutti questi componenti e l'architettura generale del computer descritta nella Panoramica , possiamo costruire un computer funzionante!

Download: - Computer Tetris finito - Script di creazione ROM, computer vuoto e computer di ricerca primaria

Il computer


49
Vorrei solo dire che le immagini in questo post sono, per qualsiasi motivo, molto belle secondo me. : P +1
HyperNeutrino,

7
Questa è la cosa più incredibile che abbia mai visto .... Vorrei fare +20 se potessi
FantaC

3
@tfbninja Puoi, si chiama taglia e puoi dare 200 reputazione.
Fabian Röling,

10
Questo processore è vulnerabile agli attacchi Spectre e Meltdown? :)
Ferrybig

5
@Ferrybig nessuna previsione sul ramo, quindi ne dubito.
JAD

621

Parte 4: QFTASM e Cogol

Panoramica sull'architettura

In breve, il nostro computer ha un'architettura RISC Harvard asincrona a 16 bit. Quando si costruisce un processore a mano, un'architettura RISC ( computer con set di istruzioni ridotto ) è praticamente un requisito. Nel nostro caso, ciò significa che il numero di codici operativi è ridotto e, cosa ancora più importante, che tutte le istruzioni vengono elaborate in modo molto simile.

Per riferimento, il computer Wireworld utilizzava un'architettura innescata dal trasporto , in cui l'unica istruzione era MOVe i calcoli venivano eseguiti scrivendo / leggendo registri speciali. Sebbene questo paradigma porti a un'architettura molto facile da implementare, il risultato è anche al limite inutilizzabile: tutte le operazioni aritmetiche / logiche / condizionali richiedono tre istruzioni. Per noi era chiaro che volevamo creare un'architettura molto meno esoterica.

Al fine di semplificare il nostro processore aumentando al contempo l'usabilità, abbiamo preso diverse importanti decisioni di progettazione:

  • Nessun registro Ogni indirizzo nella RAM viene trattato allo stesso modo e può essere utilizzato come argomento per qualsiasi operazione. In un certo senso, ciò significa che tutta la RAM potrebbe essere trattata come un registro. Ciò significa che non ci sono istruzioni speciali per il caricamento / archiviazione.
  • Allo stesso modo, mappatura della memoria. Tutto ciò che potrebbe essere scritto o letto da condivide uno schema di indirizzamento unificato. Ciò significa che il contatore del programma (PC) è l'indirizzo 0 e l'unica differenza tra le istruzioni normali e le istruzioni del flusso di controllo è che le istruzioni del flusso di controllo utilizzano l'indirizzo 0.
  • I dati sono seriali in trasmissione, paralleli in memoria. A causa della natura basata su "elettroni" del nostro computer, l'aggiunta e la sottrazione sono significativamente più facili da implementare quando i dati vengono trasmessi in forma seriale little-endian (bit meno significativo prima). Inoltre, i dati seriali eliminano la necessità di ingombranti bus di dati, che sono sia larghi che ingombranti per un corretto tempo (affinché i dati rimangano uniti, tutte le "corsie" del bus devono presentare lo stesso ritardo di viaggio).
  • Architettura di Harvard, che significa una divisione tra memoria di programma (ROM) e memoria di dati (RAM). Sebbene ciò riduca la flessibilità del processore, questo aiuta con l'ottimizzazione delle dimensioni: la lunghezza del programma è molto più grande della quantità di RAM di cui avremo bisogno, quindi possiamo dividere il programma in ROM e quindi concentrarci sulla compressione della ROM , che è molto più semplice quando è di sola lettura.
  • Larghezza dati a 16 bit. Questa è la più piccola potenza di due che è più ampia di una scheda Tetris standard (10 blocchi). Questo ci dà un intervallo di dati da -32768 a +32767 e una lunghezza massima del programma di 65536 istruzioni. (2 ^ 8 = 256 istruzioni sono sufficienti per la maggior parte delle cose che potremmo volere fare un processore giocattolo, ma non Tetris.)
  • Design asincrono. Invece di avere un orologio centrale (o, equivalentemente, diversi orologi) che detta i tempi del computer, tutti i dati sono accompagnati da un "segnale di orologio" che viaggia in parallelo con i dati mentre scorre attorno al computer. Alcuni percorsi possono essere più brevi di altri e, sebbene ciò crei difficoltà per un progetto con clock centrale, un design asincrono può facilmente gestire operazioni a tempo variabile.
  • Tutte le istruzioni hanno le stesse dimensioni. Abbiamo ritenuto che un'architettura in cui ogni istruzione avesse 1 codice operativo con 3 operandi (destinazione valore valore) fosse l'opzione più flessibile. Ciò comprende operazioni binarie di dati e spostamenti condizionali.
  • Sistema di indirizzamento semplice. Avere una varietà di modalità di indirizzamento è molto utile per supportare cose come array o ricorsione. Siamo riusciti a implementare diverse importanti modalità di indirizzamento con un sistema relativamente semplice.

Un'illustrazione della nostra architettura è contenuta nel post generale.

Funzionalità e operazioni ALU

Da qui, si trattava di determinare quale funzionalità dovrebbe avere il nostro processore. Particolare attenzione è stata prestata alla facilità di implementazione e alla versatilità di ciascun comando.

Mosse condizionate

Le mosse condizionali sono molto importanti e servono sia come flusso di controllo su piccola che su larga scala. "Piccola scala" si riferisce alla sua capacità di controllare l'esecuzione di un particolare spostamento di dati, mentre "grande scala" si riferisce al suo uso come operazione di salto condizionale per trasferire il flusso di controllo a qualsiasi pezzo arbitrario di codice. Non ci sono operazioni di salto dedicate perché, grazie alla mappatura della memoria, uno spostamento condizionale può sia copiare i dati nella RAM normale sia copiare un indirizzo di destinazione sul PC. Abbiamo anche scelto di rinunciare sia alle mosse incondizionate che ai salti incondizionati per un motivo simile: entrambi possono essere implementati come mossa condizionale con una condizione codificata su VERO.

Abbiamo scelto di avere due diversi tipi di mosse condizionate: "sposta se non zero" ( MNZ) e "sposta se meno di zero" ( MLZ). Funzionalmente, MNZequivale a verificare se un bit nei dati è un 1, mentre MLZequivale a verificare se il bit di segno è 1. Sono utili rispettivamente per le uguaglianze e i confronti. Il motivo per cui abbiamo scelto questi due rispetto ad altri come "sposta se zero" ( MEZ) o "sposta se maggiore di zero" ( MGZ) era che MEZavrebbe richiesto la creazione di un segnale VERO da un segnale vuoto, mentre MGZè un controllo più complesso, che richiede il il bit del segno è 0 mentre almeno un altro bit è 1.

Aritmetica

Le successive istruzioni più importanti, in termini di guida alla progettazione del processore, sono le operazioni aritmetiche di base. Come ho detto prima, stiamo usando dati seriali little-endian, con la scelta dell'endianness determinata dalla facilità delle operazioni di addizione / sottrazione. Avendo prima il bit meno significativo, le unità aritmetiche possono facilmente tenere traccia del bit di riporto.

Abbiamo scelto di utilizzare la rappresentazione del complemento di 2 per numeri negativi, poiché ciò rende più coerenti l'aggiunta e la sottrazione. Vale la pena notare che il computer Wireworld ha usato il complemento di 1.

L'aggiunta e la sottrazione sono l'estensione del supporto aritmetico nativo del nostro computer (oltre ai bit shift che verranno discussi più avanti). Altre operazioni, come la moltiplicazione, sono troppo complesse per essere gestite dalla nostra architettura e devono essere implementate nel software.

Operazioni bit a bit

Il nostro processore ha AND, ORe XORistruzioni che fanno ciò che ti aspetteresti. Piuttosto che avere NOTun'istruzione, abbiamo scelto di avere un'istruzione "e-non" ( ANT). La difficoltà con l' NOTistruzione è di nuovo che deve creare il segnale da una mancanza di segnale, che è difficile con gli automi cellulari. L' ANTistruzione restituisce 1 solo se il primo bit dell'argomento è 1 e il secondo bit dell'argomento è 0. Pertanto, NOT xequivale a ANT -1 x(così come XOR -1 x). Inoltre, ANTè versatile e ha il suo principale vantaggio nel mascheramento: nel caso del programma Tetris lo utilizziamo per cancellare i tetromini.

Bit Shifting

Le operazioni di spostamento dei bit sono le operazioni più complesse gestite dall'ALU. Prendono due input di dati: un valore da spostare e un importo da spostare. Nonostante la loro complessità (a causa della quantità variabile di spostamento), queste operazioni sono cruciali per molti compiti importanti, comprese le molte operazioni "grafiche" coinvolte in Tetris. I bit shift servirebbero anche da base per efficienti algoritmi di moltiplicazione / divisione.

Il nostro processore ha operazioni di shift a tre bit, "shift left" ( SL), "shift right logic" ( SRL) e "shift right arithmetic" ( SRA). I primi due turni di bit ( SLe SRL) riempiono i nuovi bit con tutti gli zeri (il che significa che un numero negativo spostato a destra non sarà più negativo). Se il secondo argomento dello spostamento è al di fuori dell'intervallo compreso tra 0 e 15, il risultato è tutto zero, come ci si potrebbe aspettare. Per l'ultimo bit shift, SRAil bit shift conserva il segno dell'ingresso e agisce quindi come una vera divisione per due.

Pipeline di istruzioni

Ora è il momento di parlare di alcuni dei dettagli grintosi dell'architettura. Ogni ciclo della CPU consiste nei seguenti cinque passaggi:

1. Recupera le istruzioni correnti dalla ROM

Il valore corrente del PC viene utilizzato per recuperare le istruzioni corrispondenti dalla ROM. Ogni istruzione ha un codice operativo e tre operandi. Ogni operando è costituito da una parola dati e una modalità di indirizzamento. Queste parti sono divise l'una dall'altra mentre vengono lette dalla ROM.

Il codice operativo è di 4 bit per supportare 16 codici operativi univoci, di cui 11 assegnati:

0000  MNZ    Move if Not Zero
0001  MLZ    Move if Less than Zero
0010  ADD    ADDition
0011  SUB    SUBtraction
0100  AND    bitwise AND
0101  OR     bitwise OR
0110  XOR    bitwise eXclusive OR
0111  ANT    bitwise And-NoT
1000  SL     Shift Left
1001  SRL    Shift Right Logical
1010  SRA    Shift Right Arithmetic
1011  unassigned
1100  unassigned
1101  unassigned
1110  unassigned
1111  unassigned

2. Scrivere il risultato (se necessario) dell'istruzione precedente nella RAM

A seconda della condizione dell'istruzione precedente (come il valore del primo argomento per uno spostamento condizionale), viene eseguita una scrittura. L'indirizzo della scrittura è determinato dal terzo operando dell'istruzione precedente.

È importante notare che la scrittura avviene dopo il recupero delle istruzioni. Ciò porta alla creazione di uno slot di ritardo del ramo in cui l'istruzione immediatamente dopo un'istruzione di ramo (qualsiasi operazione che scrive sul PC) viene eseguita al posto della prima istruzione sul bersaglio del ramo.

In alcuni casi (come i salti incondizionati), lo slot di ritardo del ramo può essere ottimizzato. In altri casi non può e le istruzioni dopo un ramo devono essere lasciate vuote. Inoltre, questo tipo di slot di ritardo significa che i rami devono usare un bersaglio di ramo che è 1 indirizzo in meno dell'effettiva istruzione di bersaglio, per tenere conto dell'incremento del PC che si verifica.

In breve, poiché l'output dell'istruzione precedente viene scritto su RAM dopo che è stata recuperata l'istruzione successiva, i salti condizionali devono avere un'istruzione vuota dopo di essi, altrimenti il ​​PC non verrà aggiornato correttamente per il salto.

3. Leggere i dati per gli argomenti dell'istruzione corrente dalla RAM

Come accennato in precedenza, ciascuno dei tre operandi è costituito sia da una parola di dati che da una modalità di indirizzamento. La parola dati è di 16 bit, la stessa larghezza della RAM. La modalità di indirizzamento è di 2 bit.

Le modalità di indirizzamento possono essere una fonte di significativa complessità per un processore come questo, poiché molte modalità di indirizzamento del mondo reale implicano calcoli in più passaggi (come l'aggiunta di offset). Allo stesso tempo, le versatili modalità di indirizzamento svolgono un ruolo importante nell'usabilità del processore.

Abbiamo cercato di unificare i concetti di utilizzo di numeri hard-coded come operandi e utilizzo di indirizzi di dati come operandi. Ciò ha portato alla creazione di modalità di indirizzamento contro-basate: la modalità di indirizzamento di un operando è semplicemente un numero che rappresenta quante volte i dati dovrebbero essere inviati attorno a un ciclo di lettura RAM. Ciò comprende l'indirizzamento immediato, diretto, indiretto e doppio indiretto.

00  Immediate:  A hard-coded value. (no RAM reads)
01  Direct:  Read data from this RAM address. (one RAM read)
10  Indirect:  Read data from the address given at this address. (two RAM reads)
11  Double-indirect: Read data from the address given at the address given by this address. (three RAM reads)

Dopo aver eseguito questa dereferenziazione, i tre operandi dell'istruzione hanno ruoli diversi. Il primo operando è in genere il primo argomento per un operatore binario, ma funge anche da condizione quando l'istruzione corrente è una mossa condizionale. Il secondo operando funge da secondo argomento per un operatore binario. Il terzo operando funge da indirizzo di destinazione per il risultato dell'istruzione.

Poiché le prime due istruzioni fungono da dati mentre la terza funge da indirizzo, le modalità di indirizzamento hanno interpretazioni leggermente diverse a seconda della posizione in cui vengono utilizzate. Ad esempio, la modalità diretta viene utilizzata per leggere i dati da un indirizzo RAM fisso (poiché è necessaria una lettura RAM), ma la modalità immediata viene utilizzata per scrivere i dati su un indirizzo RAM fisso (poiché non sono necessarie letture RAM).

4. Calcola il risultato

Il codice operativo e i primi due operandi vengono inviati all'ALU per eseguire un'operazione binaria. Per le operazioni aritmetiche, bit a bit e di spostamento, ciò significa eseguire l'operazione pertinente. Per le mosse condizionate, ciò significa semplicemente restituire il secondo operando.

Il codice operativo e il primo operando vengono utilizzati per calcolare la condizione, che determina se scrivere o meno il risultato in memoria. Nel caso di spostamenti condizionali, ciò significa determinare se un bit nell'operando è 1 (per MNZ) o determinare se il bit di segno è 1 (per MLZ). Se il codice operativo non è una mossa condizionale, la scrittura viene sempre eseguita (la condizione è sempre vera).

5. Aumentare il contatore del programma

Infine, il contatore del programma viene letto, incrementato e scritto.

A causa della posizione dell'incremento del PC tra l'istruzione letta e la scrittura dell'istruzione, ciò significa che un'istruzione che incrementa il PC di 1 non è operativa. Un'istruzione che copia il PC su se stessa fa eseguire l'istruzione successiva due volte di seguito. Ma attenzione, più istruzioni per PC in una riga possono causare effetti complessi, incluso il loop infinito, se non si presta attenzione alla pipeline di istruzioni.

Quest for Tetris Assembly

Abbiamo creato un nuovo linguaggio assembly chiamato QFTASM per il nostro processore. Questo linguaggio assembly corrisponde 1 a 1 con il codice macchina nella ROM del computer.

Qualsiasi programma QFTASM è scritto come una serie di istruzioni, una per riga. Ogni riga è formattata in questo modo:

[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]

Elenco codici op

Come discusso in precedenza, ci sono undici codici operativi supportati dal computer, ognuno dei quali ha tre operandi:

MNZ [test] [value] [dest]  – Move if Not Zero; sets [dest] to [value] if [test] is not zero.
MLZ [test] [value] [dest]  – Move if Less than Zero; sets [dest] to [value] if [test] is less than zero.
ADD [val1] [val2] [dest]   – ADDition; store [val1] + [val2] in [dest].
SUB [val1] [val2] [dest]   – SUBtraction; store [val1] - [val2] in [dest].
AND [val1] [val2] [dest]   – bitwise AND; store [val1] & [val2] in [dest].
OR [val1] [val2] [dest]    – bitwise OR; store [val1] | [val2] in [dest].
XOR [val1] [val2] [dest]   – bitwise XOR; store [val1] ^ [val2] in [dest].
ANT [val1] [val2] [dest]   – bitwise And-NoT; store [val1] & (![val2]) in [dest].
SL [val1] [val2] [dest]    – Shift Left; store [val1] << [val2] in [dest].
SRL [val1] [val2] [dest]   – Shift Right Logical; store [val1] >>> [val2] in [dest]. Doesn't preserve sign.
SRA [val1] [val2] [dest]   – Shift Right Arithmetic; store [val1] >> [val2] in [dest], while preserving sign.

Modalità di indirizzamento

Ciascuno degli operandi contiene sia un valore di dati che uno spostamento di indirizzamento. Il valore dei dati è descritto da un numero decimale compreso tra -32768 e 32767. La modalità di indirizzamento è descritta da un prefisso di una lettera al valore dei dati.

mode    name               prefix
0       immediate          (none)
1       direct             A
2       indirect           B
3       double-indirect    C 

Codice di esempio

Sequenza di Fibonacci in cinque righe:

0. MLZ -1 1 1;    initial value
1. MLZ -1 A2 3;   start loop, shift data
2. MLZ -1 A1 2;   shift data
3. MLZ -1 0 0;    end loop
4. ADD A2 A3 1;   branch delay slot, compute next term

Questo codice calcola la sequenza di Fibonacci, con l'indirizzo RAM 1 contenente il termine corrente. Trabocca rapidamente dopo il 28657.

Codice grigio:

0. MLZ -1 5 1;      initial value for RAM address to write to
1. SUB A1 5 2;      start loop, determine what binary number to covert to Gray code
2. SRL A2 1 3;      shift right by 1
3. XOR A2 A3 A1;    XOR and store Gray code in destination address
4. SUB B1 42 4;     take the Gray code and subtract 42 (101010)
5. MNZ A4 0 0;      if the result is not zero (Gray code != 101010) repeat loop
6. ADD A1 1 1;      branch delay slot, increment destination address

Questo programma calcola il codice Gray e memorizza il codice in indirizzi successivi a partire dall'indirizzo 5. Questo programma utilizza diverse funzionalità importanti come l'indirizzamento indiretto e un salto condizionale. Si interrompe una volta ottenuto il codice Gray risultante 101010, che si verifica per l'ingresso 51 all'indirizzo 56.

Interprete online

El'endia Starman ha creato un interprete online molto utile qui . È possibile scorrere il codice, impostare i punti di interruzione, eseguire scritture manuali su RAM e visualizzare la RAM come display.

Cogol

Una volta definiti il ​​linguaggio di architettura e assembly, il passo successivo sul lato "software" del progetto è stato la creazione di un linguaggio di livello superiore, qualcosa di adatto a Tetris. Così ho creato Cogol . Il nome è sia un gioco di parole su "COBOL" sia un acronimo di "C of Game of Life", anche se vale la pena notare che Cogol è in C ciò che il nostro computer è un computer reale.

Cogol esiste a un livello appena sopra il linguaggio assembly. Generalmente, la maggior parte delle linee in un programma Cogol corrispondono ciascuna a una singola linea di assemblaggio, ma ci sono alcune caratteristiche importanti del linguaggio:

  • Le funzionalità di base includono variabili denominate con assegnazioni e operatori con sintassi più leggibile. Ad esempio, ADD A1 A2 3diventa z = x + y;, con il compilatore che associa le variabili agli indirizzi.
  • Looping costrutti quali if(){}, while(){}e do{}while();quindi il compilatore maniglie ramificazione.
  • Matrici unidimensionali (con puntatore aritmetico), utilizzate per la scheda Tetris.
  • Subroutine e stack di chiamate. Questi sono utili per prevenire la duplicazione di grossi blocchi di codice e per supportare la ricorsione.

Il compilatore (che ho scritto da zero) è molto semplice / ingenuo, ma ho tentato di ottimizzare a mano diversi costrutti del linguaggio per ottenere una breve durata del programma compilato.

Ecco alcune brevi rassegne di come funzionano le varie funzionalità linguistiche:

tokenizzazione

Il codice sorgente è tokenizzato linearmente (single-pass), usando semplici regole su quali caratteri possono essere adiacenti all'interno di un token. Quando viene incontrato un personaggio che non può essere adiacente all'ultimo carattere del token corrente, il token corrente viene considerato completo e il nuovo personaggio inizia un nuovo token. Alcuni personaggi (come {o ,) non possono essere adiacenti ad altri personaggi e sono quindi i loro token. Altri (come >o =) possono solo essere adiacente ad altri caratteri all'interno loro classe, e possono quindi formare token come >>>, ==o >=, ma non come =2. I personaggi degli spazi bianchi forza un confine tra i token ma non sono essi stessi inclusi nel risultato. Il personaggio più difficile da tokenizzare è- perché può rappresentare sia la sottrazione che la negazione unaria e quindi richiede un involucro speciale.

parsing

L'analisi viene eseguita anche in un solo passaggio. Il compilatore ha metodi per gestire ciascuno dei diversi costrutti del linguaggio e i token vengono estratti dall'elenco globale di token mentre vengono utilizzati dai vari metodi del compilatore. Se il compilatore vede mai un token che non si aspetta, genera un errore di sintassi.

Allocazione di memoria globale

Il compilatore assegna a ciascuna variabile globale (parola o matrice) i propri indirizzi RAM designati. È necessario dichiarare tutte le variabili utilizzando la parola chiave in mymodo che il compilatore sappia allocare spazio per essa. Molto più interessante delle variabili globali nominate è la gestione della memoria dell'indirizzo scratch. Molte istruzioni (in particolare condizionali e molti accessi all'array) richiedono indirizzi temporanei "scratch" per memorizzare calcoli intermedi. Durante il processo di compilazione, il compilatore alloca e disalloca gli indirizzi scratch, se necessario. Se il compilatore necessita di più indirizzi scratch, dedicherà più RAM come indirizzi scratch. Credo che sia tipico per un programma richiedere solo pochi indirizzi scratch, sebbene ogni indirizzo scratch verrà utilizzato più volte.

IF-ELSE dichiarazioni

La sintassi per le if-elseistruzioni è il modulo C standard:

other code
if (cond) {
  first body
} else {
  second body
}
other code

Quando convertito in QFTASM, il codice è organizzato in questo modo:

other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)

Se il primo corpo viene eseguito, il secondo corpo viene ignorato. Se il primo corpo viene ignorato, viene eseguito il secondo corpo.

Nell'assemblaggio, un test di condizione è in genere solo una sottrazione e il segno del risultato determina se eseguire il salto o eseguire il corpo. Un'istruzione MLZviene utilizzata per gestire le disuguaglianze come >o <=. Un'istruzione MNZviene utilizzata per gestire ==, poiché salta sul corpo quando la differenza non è zero (e quindi quando gli argomenti non sono uguali). I condizionali multi-espressione non sono attualmente supportati.

Se l' elseistruzione viene omessa, viene omesso anche il salto incondizionato e il codice QFTASM è simile al seguente:

other code
condition test
conditional jump
body
other code (conditional jump target)

WHILE dichiarazioni

La sintassi per le whileistruzioni è anche il modulo C standard:

other code
while (cond) {
  body
}
other code

Quando convertito in QFTASM, il codice è organizzato in questo modo:

other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code

Il test delle condizioni e il salto condizionale si trovano alla fine del blocco, il che significa che vengono rieseguiti dopo ogni esecuzione del blocco. Quando la condizione viene restituita falsa, il corpo non viene ripetuto e il ciclo termina. Durante l'avvio dell'esecuzione del ciclo, il flusso di controllo passa sul corpo del ciclo al codice della condizione, quindi il corpo non viene mai eseguito se la condizione è falsa la prima volta.

Un'istruzione MLZviene utilizzata per gestire le disuguaglianze come >o <=. Diversamente dalle ifistruzioni, MNZviene utilizzata un'istruzione per gestire !=, poiché salta al corpo quando la differenza non è zero (e quindi quando gli argomenti non sono uguali).

DO-WHILE dichiarazioni

L'unica differenza tra whilee do-whileè che il do-whilecorpo di un loop non viene inizialmente ignorato, quindi viene sempre eseguito almeno una volta. In genere utilizzo le do-whileistruzioni per salvare un paio di righe di codice assembly quando so che il loop non dovrà mai essere ignorato del tutto.

Array

Le matrici unidimensionali sono implementate come blocchi contigui di memoria. Tutti gli array sono a lunghezza fissa in base alla loro dichiarazione. Le matrici sono dichiarate così:

my alpha[3];               # empty array
my beta[11] = {3,2,7,8};   # first four elements are pre-loaded with those values

Per l'array, questa è una possibile mappatura RAM, che mostra come gli indirizzi 15-18 sono riservati all'array:

15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]

L'indirizzo etichettato alphaviene riempito con un puntatore alla posizione di alpha[0], quindi in questo caso l'indirizzo 15 contiene il valore 16. La alphavariabile può essere utilizzata all'interno del codice Cogol, possibilmente come puntatore di stack se si desidera utilizzare questo array come stack .

L'accesso agli elementi di un array avviene con la array[index]notazione standard . Se il valore di indexè una costante, questo riferimento viene automaticamente inserito con l'indirizzo assoluto di quell'elemento. Altrimenti esegue qualche aritmetica del puntatore (solo aggiunta) per trovare l'indirizzo assoluto desiderato. È anche possibile nidificare l'indicizzazione, ad esempio alpha[beta[1]].

Subroutine e chiamate

Le subroutine sono blocchi di codice che possono essere richiamati da più contesti, impedendo la duplicazione del codice e consentendo la creazione di programmi ricorsivi. Ecco un programma con una subroutine ricorsiva per generare numeri di Fibonacci (sostanzialmente l'algoritmo più lento):

# recursively calculate the 10th Fibonacci number
call display = fib(10).sum;
sub fib(cur,sum) {
  if (cur <= 2) {
    sum = 1;
    return;
  }
  cur--;
  call sum = fib(cur).sum;
  cur--;
  call sum += fib(cur).sum;
}

Una subroutine viene dichiarata con la parola chiave sube una subroutine può essere posizionata ovunque all'interno del programma. Ogni subroutine può avere più variabili locali, che sono dichiarate come parte del suo elenco di argomenti. A questi argomenti possono anche essere dati valori predefiniti.

Per gestire le chiamate ricorsive, le variabili locali di una subroutine sono memorizzate nello stack. L'ultima variabile statica nella RAM è il puntatore dello stack di chiamate e tutta la memoria successiva funge da stack di chiamate. Quando viene chiamata una subroutine, viene creato un nuovo frame nello stack di chiamate, che include tutte le variabili locali e l'indirizzo di ritorno (ROM). A ogni subroutine nel programma viene assegnato un singolo indirizzo RAM statico che funge da puntatore. Questo puntatore indica la posizione della chiamata "corrente" della subroutine nello stack di chiamate. Il riferimento a una variabile locale viene eseguito utilizzando il valore di questo puntatore statico più un offset per fornire l'indirizzo di quella particolare variabile locale. Anche nello stack di chiamate è contenuto il valore precedente del puntatore statico. Qui'

RAM map:
0: pc
1: display
2: scratch0
3: fib
4: scratch1
5: scratch2
6: scratch3
7: call

fib map:
0: return
1: previous_call
2: cur
3: sum

Una cosa interessante delle subroutine è che non restituiscono alcun valore particolare. Piuttosto, tutte le variabili locali della subroutine possono essere lette dopo l'esecuzione della subroutine, quindi una varietà di dati può essere estratta da una chiamata di subroutine. Ciò si ottiene memorizzando il puntatore per quella chiamata specifica della subroutine, che può quindi essere utilizzata per recuperare qualsiasi variabile locale all'interno del frame dello stack (recentemente deallocato).

Esistono diversi modi per chiamare una subroutine, tutti utilizzando la callparola chiave:

call fib(10);   # subroutine is executed, no return vaue is stored

call pointer = fib(10);   # execute subroutine and return a pointer
display = pointer.sum;    # access a local variable and assign it to a global variable

call display = fib(10).sum;   # immediately store a return value

call display += fib(10).sum;   # other types of assignment operators can also be used with a return value

Qualsiasi numero di valori può essere fornito come argomento per una chiamata di subroutine. Qualsiasi argomento non fornito verrà compilato con l'eventuale valore predefinito. Un argomento non fornito e privo di valore predefinito non viene cancellato (per risparmiare istruzioni / tempo), pertanto potrebbe potenzialmente assumere qualsiasi valore all'inizio della subroutine.

I puntatori sono un modo per accedere a più variabili locali della subroutine, anche se è importante notare che il puntatore è solo temporaneo: i dati a cui punta il puntatore verranno distrutti quando viene effettuata un'altra chiamata di subroutine.

Etichette di debug

Qualsiasi {...}blocco di codice in un programma Cogol può essere preceduto da un'etichetta descrittiva composta da più parole. Questa etichetta è allegata come commento nel codice assembly compilato e può essere molto utile per il debug in quanto semplifica l'individuazione di blocchi di codice specifici.

Ottimizzazione degli slot per ritardo di derivazione

Al fine di migliorare la velocità del codice compilato, il compilatore Cogol esegue alcune ottimizzazioni di slot di ritardo davvero basilari come passaggio finale sul codice QFTASM. Per qualsiasi salto incondizionato con uno slot di ritardo ramo vuoto, lo slot di ritardo può essere riempito dalla prima istruzione nella destinazione di salto e la destinazione di salto viene incrementata di uno per indicare l'istruzione successiva. Questo generalmente salva un ciclo ogni volta che viene eseguito un salto incondizionato.

Scrivere il codice Tetris in Cogol

Il programma finale di Tetris è stato scritto in Cogol e il codice sorgente è disponibile qui . Il codice QFTASM compilato è disponibile qui . Per comodità, un collegamento permanente è disponibile qui: Tetris in QFTASM . Poiché l'obiettivo era golfare il codice assembly (non il codice Cogol), il codice Cogol risultante è ingombrante. Molte parti del programma si troverebbero normalmente nelle subroutine, ma quelle subroutine erano effettivamente abbastanza brevi da duplicare il codice salvando le istruzioni sulcalldichiarazioni. Il codice finale ha solo una subroutine oltre al codice principale. Inoltre, molti array sono stati rimossi e sostituiti con un elenco equivalentemente lungo di singole variabili o con molti numeri hardcoded nel programma. Il codice QFTASM compilato finale ha meno di 300 istruzioni, sebbene sia solo leggermente più lungo della fonte Cogol stessa.


22
Adoro il fatto che la scelta delle istruzioni del linguaggio assembly sia definita dall'hardware del substrato (nessun MEZ perché è difficile assemblare un vero da due falsi). Lettura fantastica
AlexC

1
Hai detto che =può stare solo accanto a se stesso, ma c'è un !=.
Fabian Röling il

@Fabian and a+=
Oliphaunt

@Oliphaunt Sì, la mia descrizione non è stata abbastanza accurata, è più una cosa di classe di carattere, in cui una certa classe di personaggi può essere adiacente l'uno all'altro.
PhiNotPi

606

Parte 5: Assemblea, traduzione e futuro

Con il nostro programma di assemblaggio dal compilatore, è tempo di assemblare una ROM per il computer Varlife e tradurre tutto in un grande schema GoL!

montaggio

L'assemblaggio del programma di assemblaggio in una ROM avviene in modo analogo alla programmazione tradizionale: ogni istruzione viene tradotta in un equivalente binario e questi vengono poi concatenati in un grande BLOB binario che chiamiamo eseguibile. Per noi, l'unica differenza è che il BLOB binario deve essere tradotto in circuiti Varlife e collegato al computer.

K Zhang ha scritto CreateROM.py , uno script Python per Golly che esegue l'assemblaggio e la traduzione. È abbastanza semplice: prende un programma di assemblaggio dagli Appunti, lo assembla in un binario e lo traduce in un circuito. Ecco un esempio con un semplice tester di primalità incluso nello script:

#0. MLZ -1 3 3;
#1. MLZ -1 7 6; preloadCallStack
#2. MLZ -1 2 1; beginDoWhile0_infinite_loop
#3. MLZ -1 1 4; beginDoWhile1_trials
#4. ADD A4 2 4;
#5. MLZ -1 A3 5; beginDoWhile2_repeated_subtraction
#6. SUB A5 A4 5;
#7. SUB 0 A5 2;
#8. MLZ A2 5 0;
#9. MLZ 0 0 0; endDoWhile2_repeated_subtraction
#10. MLZ A5 3 0;
#11. MNZ 0 0 0; endDoWhile1_trials
#12. SUB A4 A3 2;
#13. MNZ A2 15 0; beginIf3_prime_found
#14. MNZ 0 0 0;
#15. MLZ -1 A3 1; endIf3_prime_found
#16. ADD A3 2 3;
#17. MLZ -1 3 0;
#18. MLZ -1 1 4; endDoWhile0_infinite_loop

Questo produce il seguente binario:

0000000000000001000000000000000000010011111111111111110001
0000000000000000000000000000000000110011111111111111110001
0000000000000000110000000000000000100100000000000000110010
0000000000000000010100000000000000110011111111111111110001
0000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000011110100000000000000100000
0000000000000000100100000000000000110100000000000001000011
0000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000110100000000000001010001
0000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000001010100000000000000100001
0000000000000000100100000000000001010000000000000000000011
0000000000000001010100000000000001000100000000000001010011
0000000000000001010100000000000000110011111111111111110001
0000000000000001000000000000000000100100000000000001000010
0000000000000001000000000000000000010011111111111111110001
0000000000000000010000000000000000100011111111111111110001
0000000000000001100000000000000001110011111111111111110001
0000000000000000110000000000000000110011111111111111110001

Quando tradotto in circuiti Varlife, si presenta così:

rom

primo piano ROM

La ROM viene quindi collegata al computer, che costituisce un programma perfettamente funzionante in Varlife. Ma non abbiamo ancora finito ...

Traduzione di Game of Life

Per tutto questo tempo, abbiamo lavorato in vari strati di astrazione sopra la base di Game of Life. Ma ora è il momento di tirare indietro il sipario dell'astrazione e tradurre il nostro lavoro in un modello di Game of Life. Come accennato in precedenza, stiamo usando OTCA Metapixel come base per Varlife. Quindi, il passaggio finale è convertire ogni cella in Varlife in un metapixel in Game of Life.

Per fortuna, Golly viene fornito con uno script ( metafier.py ) che può convertire i pattern in diversi set di regole in pattern di Game of Life tramite OTCA Metapixel. Sfortunatamente, è progettato solo per convertire modelli con un unico set di regole globale, quindi non funziona su Varlife. Ho scritto una versione modificata che risolve tale problema, in modo che la regola per ciascun metapixel sia generata cella per cella per Varlife.

Quindi, il nostro computer (con la ROM Tetris) ha un riquadro di delimitazione di 1.436 x 5.082. Delle 7.297.752 celle in quella scatola, 6.075.811 sono spazi vuoti, lasciando un conteggio effettivo della popolazione di 1.221.941. Ognuna di queste celle deve essere tradotta in un metapixel OTCA, che ha un riquadro di delimitazione di 2048x2048 e una popolazione di 64.691 (per un metapixel ON) o 23.920 (per un metapixel OFF). Ciò significa che il prodotto finale avrà un riquadro di delimitazione di 2.940.928 x 10.407.936 (oltre a qualche migliaio in più per i confini dei metapixel), con una popolazione compresa tra 29.228.828.720 e 79.048.585.231. Con 1 bit per cella live, tra 27 e 74 GiB sono necessari per rappresentare l'intero computer e la ROM.

Ho incluso questi calcoli qui perché ho trascurato di eseguirli prima di avviare lo script e ho esaurito molto rapidamente la memoria sul mio computer. Dopo un killcomando in preda al panico , ho apportato una modifica allo script metafier. Ogni 10 righe di metapixel, il modello viene salvato su disco (come file RLE compresso con gzip) e la griglia viene svuotata. Ciò aggiunge ulteriore runtime alla traduzione e utilizza più spazio su disco, ma mantiene l'utilizzo della memoria entro limiti accettabili. Poiché Golly utilizza un formato RLE esteso che include la posizione del modello, ciò non aggiunge ulteriore complessità al caricamento del modello: basta aprire tutti i file del modello sullo stesso livello.

K Zhang si è basato su questo lavoro e ha creato uno script metafier più efficiente che utilizza il formato di file MacroCell, che è molto più efficiente di RLE per modelli di grandi dimensioni. Questo script funziona in modo considerevolmente più veloce (pochi secondi, rispetto a più ore per lo script metafier originale), crea un output notevolmente più piccolo (121 KB contro 1,7 GB) e può metafy l'intero computer e la ROM in un colpo solo senza usare una quantità enorme di memoria. Sfrutta il fatto che i file MacroCell codificano alberi che descrivono i modelli. Utilizzando un file modello personalizzato, i metapixel vengono precaricati nell'albero e, dopo alcuni calcoli e modifiche per il rilevamento dei vicini, è possibile semplicemente aggiungere il modello Varlife.

Il file di pattern dell'intero computer e della ROM in Game of Life è disponibile qui .


Il futuro del progetto

Ora che abbiamo creato Tetris, abbiamo finito, giusto? Neanche vicino. Abbiamo diversi altri obiettivi per questo progetto a cui stiamo lavorando:

  • muddyfish e Kritixi Lithos stanno continuando a lavorare sul linguaggio di livello superiore compilabile con QFTASM.
  • El'endia Starman sta lavorando agli aggiornamenti dell'interprete QFTASM online.
  • quartata sta lavorando su un backend GCC, che consentirà la compilazione di codice C e C ++ indipendente (e potenzialmente altre lingue, come Fortran, D o Objective-C) in QFTASM tramite GCC. Ciò consentirà di creare programmi più sofisticati in un linguaggio più familiare, sebbene senza una libreria standard.
  • Uno dei maggiori ostacoli che dobbiamo superare prima di poter compiere ulteriori progressi è il fatto che i nostri strumenti non possono emettere codice indipendente dalla posizione (ad esempio salti relativi). Senza PIC, non possiamo fare alcun collegamento e quindi perdiamo i vantaggi che derivano dalla possibilità di collegarsi a librerie esistenti. Stiamo lavorando per cercare di trovare un modo per eseguire correttamente i PIC.
  • Stiamo discutendo il prossimo programma che vogliamo scrivere per il computer QFT. In questo momento, Pong sembra un bel gol.

2
Solo guardando alla sottosezione futura, un salto relativo non è solo un ADD PC offset PC? Scusa la mia ingenuità se ciò non è corretto, la programmazione dell'assemblaggio non è mai stata il mio punto di forza.
MBraedley,

3
@Timmmm Sì, ma molto lentamente. (Devi anche usare HashLife).
uno spaghetto il

75
Il prossimo programma per cui scrivi dovrebbe essere Conway's Game of Life.
ACK_stoverflow

13
@ACK_stoverflow Ciò avverrà ad un certo punto.
Mego

13
Hai un video in esecuzione?
PyRulez,

583

Parte 6: il compilatore più recente di QFTASM

Sebbene Cogol sia sufficiente per un'implementazione rudimentale di Tetris, è troppo semplice e di livello troppo basso per la programmazione generale a un livello facilmente leggibile. Abbiamo iniziato a lavorare su una nuova lingua a settembre 2016. I progressi nella lingua sono stati lenti a causa di bug difficili da comprendere e della vita reale.

Abbiamo creato un linguaggio di basso livello con sintassi simile a Python, incluso un sistema di tipo semplice, subroutine che supportano la ricorsione e operatori in linea. Il compilatore dal testo a QFTASM è stato creato con 4 passaggi: il tokeniser, l'albero della grammatica, un compilatore di alto livello e un compilatore di basso livello.

Il tokenizzatore

Lo sviluppo è stato avviato utilizzando Python utilizzando la libreria di tokeniser integrata, il che significa che questo passaggio è stato piuttosto semplice. Sono state necessarie solo alcune modifiche all'output predefinito, inclusi i commenti di rimozione (ma non #includei commenti ).

L'albero della grammatica

L'albero della grammatica è stato creato per essere facilmente estensibile senza dover modificare alcun codice sorgente.

La struttura ad albero è memorizzata in un file XML che include la struttura dei nodi che possono comporre l'albero e il modo in cui sono costituiti con altri nodi e token.

La grammatica necessaria per supportare i nodi ripetuti e quelli opzionali. Ciò è stato ottenuto introducendo meta tag per descrivere come i token dovevano essere letti.

I token che vengono generati vengono quindi analizzati attraverso le regole della grammatica in modo tale che l'output formi un albero di elementi grammaticali come subs e generic_variables, che a loro volta contengono altri elementi grammaticali e token.

Compilazione in codice di alto livello

Ogni caratteristica del linguaggio deve poter essere compilata in costrutti di alto livello. Questi includono assign(a, 12) e call_subroutine(is_prime, call_variable=12, return_variable=temp_var). Caratteristiche come l'inserimento di elementi vengono eseguite in questo segmento. Questi sono definiti come se operatorsono speciali in quanto sono allineati ogni volta che un operatore come +o %viene utilizzato. Per questo motivo, sono più limitati del codice normale: non possono utilizzare il proprio operatore né alcun operatore che si basi su quello che viene definito.

Durante il processo di allineamento, le variabili interne vengono sostituite con quelle che vengono chiamate. Questo in effetti gira

operator(int a + int b) -> int c
    return __ADD__(a, b)
int i = 3+3

dentro, come moto a luogo, andare da dentro a fuori: I put my hand inTO my pocket = metto la mano in tasca

int i = __ADD__(3, 3)

Questo comportamento tuttavia può essere dannoso e soggetto a bug se la variabile di input e le variabili di output puntano nella stessa posizione in memoria. Per utilizzare un comportamento "più sicuro", la unsafeparola chiave regola il processo di compilazione in modo tale che vengano create e copiate ulteriori variabili da e in linea, se necessario.

Variabili di scratch e operazioni complesse

Operazioni matematiche come quelle a += (b + c) * 4non possono essere calcolate senza l'utilizzo di celle di memoria aggiuntive. Il compilatore di alto livello si occupa di questo separando le operazioni in diverse sezioni:

scratch_1 = b + c
scratch_1 = scratch_1 * 4
a = a + scratch_1

Ciò introduce il concetto di variabili scratch che vengono utilizzate per la memorizzazione di informazioni intermedie di calcoli. Sono assegnati come richiesto e distribuiti nel pool generale una volta terminato. Ciò riduce il numero di posizioni di memoria scratch necessarie per l'uso. Le variabili scratch sono considerate globali.

Ogni subroutine ha il proprio VariableStore per mantenere un riferimento a tutte le variabili utilizzate dalla subroutine e al loro tipo. Alla fine della compilazione, vengono tradotti in offset relativi dall'inizio del negozio e quindi dati gli indirizzi effettivi nella RAM.

Struttura RAM

Program counter
Subroutine locals
Operator locals (reused throughout)
Scratch variables
Result variable
Stack pointer
Stack
...

Compilazione di basso livello

Le uniche cose che il compilatore di basso livello ha a che fare con sono sub, call_sub, return, assign, ife while. Questo è un elenco molto ridotto di attività che possono essere tradotte più facilmente nelle istruzioni QFTASM.

sub

Questo individua l'inizio e la fine di una subroutine denominata. Il compilatore di basso livello aggiunge etichette e, nel caso della mainsubroutine, aggiunge un'istruzione di uscita (passa alla fine della ROM).

if e while

Sia l' whilee ifinterpreti di basso livello sono abbastanza semplici: ottengono i puntatori alle loro condizioni e saltare a seconda della loro. whilei loop sono leggermente diversi in quanto sono compilati come

...
condition
jump to check
code
condition
if condtion: jump to code
...

call_sub e return

A differenza della maggior parte delle architetture, il computer per cui stiamo compilando non ha il supporto hardware per il push e il popping da uno stack. Ciò significa che sia spingendo che saltando fuori dalla pila prendono due istruzioni. In caso di popping, diminuiamo il puntatore dello stack e copiamo il valore su un indirizzo. Nel caso di push, copiamo un valore da un indirizzo all'indirizzo nel puntatore dello stack corrente e quindi incrementiamo.

Tutti i locali per una subroutine sono memorizzati in una posizione fissa nella RAM determinata al momento della compilazione. Per far funzionare la ricorsione, tutti i locali di una funzione vengono inseriti nello stack all'inizio di una chiamata. Quindi gli argomenti della subroutine vengono copiati nella loro posizione nell'archivio locale. Il valore dell'indirizzo di ritorno viene messo nello stack e la subroutine viene eseguita.

Quando returnviene rilevata un'istruzione, la parte superiore dello stack viene espulsa e il contatore del programma viene impostato su quel valore. I valori per i locali della subroutine chiamante vengono estratti dalla pila e nella loro posizione precedente.

assign

Le assegnazioni di variabili sono le cose più semplici da compilare: prendono una variabile e un valore e si compilano nella singola riga: MLZ -1 VALUE VARIABLE

Assegnare obiettivi di salto

Infine, il compilatore elabora gli obiettivi di salto per le etichette allegate alle istruzioni. Viene determinata la posizione assoluta delle etichette e quindi i riferimenti a tali etichette vengono sostituiti con tali valori. Le etichette stesse vengono rimosse dal codice e infine i numeri delle istruzioni vengono aggiunti al codice compilato.

Esempio di compilazione passo-passo

Ora che abbiamo attraversato tutte le fasi, passiamo attraverso un processo di compilazione reale per un programma reale, passo dopo passo.

#include stdint

sub main
    int a = 8
    int b = 12
    int c = a * b

Ok, abbastanza semplice. Dovrebbe essere ovvio che, alla fine del programma, a = 8, b = 12, c = 96. Innanzitutto, includiamo le parti rilevanti di stdint.txt:

operator (int a + int b) -> int
    return __ADD__(a, b)

operator (int a - int b) -> int
    return __SUB__(a, b)

operator (int a < int b) -> bool
    bool rtn = 0
    rtn = __MLZ__(a-b, 1)
    return rtn

unsafe operator (int a * int b) -> int
    int rtn = 0
    for (int i = 0; i < b; i+=1)
        rtn += a
    return rtn

sub main
    int a = 8
    int b = 12
    int c = a * b

Ok, leggermente più complicato. Passiamo al tokenizzatore e vediamo cosa viene fuori. In questa fase, avremo solo un flusso lineare di token senza alcuna forma di struttura

NAME NAME operator
LPAR OP (
NAME NAME int
NAME NAME a
PLUS OP +
NAME NAME int
NAME NAME b
RPAR OP )
OP OP ->
NAME NAME int
NEWLINE NEWLINE
INDENT INDENT     
NAME NAME return
NAME NAME __ADD__
LPAR OP (
NAME NAME a
COMMA OP ,
NAME NAME b
RPAR OP )
...

Ora tutti i token vengono sottoposti al parser grammaticale e generano un albero con i nomi di ciascuna delle sezioni. Questo mostra la struttura di alto livello come letto dal codice.

GrammarTree file
 'stmts': [GrammarTree stmts_0
  '_block_name': 'inline'
  'inline': GrammarTree inline
   '_block_name': 'two_op'
   'type_var': GrammarTree type_var
    '_block_name': 'type'
    'type': 'int'
    'name': 'a'
    '_global': False

   'operator': GrammarTree operator
    '_block_name': '+'

   'type_var_2': GrammarTree type_var
    '_block_name': 'type'
    'type': 'int'
    'name': 'b'
    '_global': False
   'rtn_type': 'int'
   'stmts': GrammarTree stmts
    ...

Questo albero grammaticale imposta le informazioni che devono essere analizzate dal compilatore di alto livello. Include informazioni quali tipi di struttura e attributi di una variabile. La struttura grammaticale prende quindi queste informazioni e assegna le variabili necessarie per le subroutine. L'albero inserisce anche tutte le linee.

('sub', 'start', 'main')
('assign', int main_a, 8)
('assign', int main_b, 12)
('assign', int op(*:rtn), 0)
('assign', int op(*:i), 0)
('assign', global bool scratch_2, 0)
('call_sub', '__SUB__', [int op(*:i), int main_b], global int scratch_3)
('call_sub', '__MLZ__', [global int scratch_3, 1], global bool scratch_2)
('while', 'start', 1, 'for')
('call_sub', '__ADD__', [int op(*:rtn), int main_a], int op(*:rtn))
('call_sub', '__ADD__', [int op(*:i), 1], int op(*:i))
('assign', global bool scratch_2, 0)
('call_sub', '__SUB__', [int op(*:i), int main_b], global int scratch_3)
('call_sub', '__MLZ__', [global int scratch_3, 1], global bool scratch_2)
('while', 'end', 1, global bool scratch_2)
('assign', int main_c, int op(*:rtn))
('sub', 'end', 'main')

Successivamente, il compilatore di basso livello deve convertire questa rappresentazione di alto livello in codice QFTASM. Alle variabili vengono assegnate posizioni nella RAM in questo modo:

int program_counter
int op(*:i)
int main_a
int op(*:rtn)
int main_c
int main_b
global int scratch_1
global bool scratch_2
global int scratch_3
global int scratch_4
global int <result>
global int <stack>

Le semplici istruzioni vengono quindi compilate. Infine, vengono aggiunti i numeri delle istruzioni, con conseguente codice QFTASM eseguibile.

0. MLZ 0 0 0;
1. MLZ -1 12 11;
2. MLZ -1 8 2;
3. MLZ -1 12 5;
4. MLZ -1 0 3;
5. MLZ -1 0 1;
6. MLZ -1 0 7;
7. SUB A1 A5 8;
8. MLZ A8 1 7;
9. MLZ -1 15 0;
10. MLZ 0 0 0;
11. ADD A3 A2 3;
12. ADD A1 1 1;
13. MLZ -1 0 7;
14. SUB A1 A5 8;
15. MLZ A8 1 7;
16. MNZ A7 10 0;
17. MLZ 0 0 0;
18. MLZ -1 A3 4;
19. MLZ -1 -2 0;
20. MLZ 0 0 0;

La sintassi

Ora che abbiamo la lingua nuda, dobbiamo effettivamente scrivere un piccolo programma al suo interno. Stiamo usando il rientro come fa Python, suddividendo i blocchi logici e controllando il flusso. Ciò significa che gli spazi bianchi sono importanti per i nostri programmi. Ogni programma completo ha una mainsubroutine che si comporta proprio come la main()funzione in linguaggi simili a C. La funzione viene eseguita all'inizio del programma.

Variabili e tipi

Quando le variabili vengono definite per la prima volta, è necessario che siano associate a un tipo. I tipi attualmente definiti sono inte boolcon la sintassi definita per gli array ma non il compilatore.

Biblioteche e operatori

È stdint.txtdisponibile una libreria chiamata che definisce gli operatori di base. Se questo non è incluso, nemmeno gli operatori semplici saranno definiti. Possiamo usare questa libreria con #include stdint. stdintdefinisce operatori come +, >>e persino *e %, nessuno dei quali sono codici operativi diretti QFTASM.

La lingua consente inoltre di chiamare direttamente i codici operativi QFTASM __OPCODENAME__.

L'aggiunta in stdintè definita come

operator (int a + int b) -> int
    return __ADD__(a, b)

Il che definisce cosa fa l' +operatore quando riceve due intsecondi.


1
Posso chiedermi, perché è stato deciso di creare una CA simile a wireworld nel gioco della vita di Conway e creare un nuovo processore usando questo circuito anziché riutilizzare / aggiornare un computer universale cgol esistente come questo ?
eaglgenes101,

4
@ eaglgenes101 Per cominciare, non penso che molti di noi fossero a conoscenza dell'esistenza di altri computer universali utilizzabili. L'idea di una CA simile a wireworld con più regole miste è nata come risultato del gioco con metacell (credo - Phi è stato colui che ha avuto l'idea). Da lì, è stata una progressione logica a ciò che abbiamo creato.
Mego
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.