Un piccolo linguaggio di tipo C che le macchine di turing possono simulare


11

Sto cercando un linguaggio di piccole dimensioni che aiuti a "convincere" gli studenti che i turing machine sono un modello di calcolo sufficientemente generale. Cioè, una lingua che assomiglia alle lingue a cui sono abituati, ma è anche facile da simulare su una macchina da turismo.

Papadimitriou usa macchine RAM per questo lavoro, ma temo che paragonare qualcosa di strano (come una macchina da turismo) a un'altra cosa strana (fondamentalmente, un linguaggio di assemblaggio) sarebbe troppo poco convincente per molti studenti.

Eventuali suggerimenti sarebbero i benvenuti (specialmente se forniti con documentazione consigliata)


7
C'è una ragione per cui i computer erano originariamente programmati in linguaggio assembly ... scrivere compilatori o interpreti non è banale . E scrivere compilatori o interpreti per le macchine di Turing è probabilmente ancora più difficile.
Peter Shor,

deve essere in qualche modo in disaccordo con PS, un compilatore TM non è molto più difficile rispetto ad esempio alla conversione di istanze di factoring in SAT o altri esercizi quasi universitari. vedi anche i migliori simulatori di macchine per turing sul web . ecco un esempio di un compilatore di macchine Turing scritto in ruby ​​con codice sorgente di esempio (per il linguaggio di alto livello). purtroppo non sembrano essere più raffinati disponibili. sarebbe un grande progetto open source.
vzn

2
@OmarShehab, Una modifica porta la domanda alla prima pagina. Non modificare la vecchia domanda quando la modifica non migliora in modo significativo la domanda. Anche la modifica di un gran numero di domande che non si trovano nella prima pagina non è buona in quanto espelle nuove domande dalla prima pagina. Grazie.
Kaveh,

@kaveh capito.
Omar Shehab,

Risposte:


15
  • Se i tuoi studenti hanno svolto una programmazione funzionale, l'approccio migliore che conosco è quello di iniziare con il calcolo lambda non tipizzato, quindi utilizzare il teorema di astrazione della parentesi per tradurlo in combinatori SKI. Quindi, è possibile utilizzare la e u t m teoremi per mostrare che le macchine di Turing formano un algebra combinatoria parzialeSmnutm , e così in grado di interpretare i combinatori sci.

    Dubito che questo sia l'approccio più semplice possibile, ma mi piace come poggia su alcuni dei teoremi fondamentali della calcolabilità (che potresti voler coprire per altri motivi).

    Sembra che Andrej Bauer abbia risposto a una domanda simile su Mathoverflow qualche mese fa.

  • Se sei impostato su un linguaggio di tipo C, il tuo percorso sarà molto più ruvido, poiché hanno una semantica piuttosto complicata - dovrai

    1. Mostra che le macchine Turing possono simulare contemporaneamente una pila e una pila e
    2. Mostra come le variabili possono essere implementate con uno stack e
    3. Mostra che le chiamate di procedura possono essere implementate con uno stack.

    Questo è molto del contenuto di una classe di compilatori, onestamente.


7

Il mio professore di Teoria del Comp a undergrad ha iniziato dimostrando che una macchina Turing a nastro singolo può implementare una macchina Turing a nastro multiplo. Gestisce la dichiarazione delle variabili: se un programma ha sei dichiarazioni di variabili, può essere facilmente implementato su una macchina Turing a sette nastri (un nastro per ogni variabile e un nastro "registra" per aiutare a svolgere attività come l'aritmetica e il controllo dell'uguaglianza tra nastri). Quindi ha mostrato come implementare i cicli FOR e WHILE di base, e a quel punto avevamo un linguaggio di tipo C di tipo Turing completo. L'ho trovato soddisfacente, comunque.


6

Sto pensando proprio ora a come convincermi che le macchine di Turing sono un modello generale di calcolo. Concordo sul fatto che il trattamento standard della tesi di Church-Turing in alcuni libri di testo standard, ad esempio Sipser, non sia molto completo. Ecco uno schizzo di come potrei passare dalle macchine di Turing a un linguaggio di programmazione più riconoscibile.

Prendi in considerazione un linguaggio di programmazione strutturato a blocchi con ife whileistruzioni, con funzioni e subroutine definite non ricorsive , con variabili casuali booleane denominate ed espressioni booleane generali e con un singolo array booleano illimitato tape[n]con un puntatore di array integer nche può essere incrementato o decrementato, n++oppure n--. Il puntatore nè inizialmente zero e l'arraytape è inizialmente tutto zero. Quindi, questo linguaggio del computer può essere simile a C o Python, ma è molto limitato nei suoi tipi di dati. In realtà, sono così limitati che non abbiamo nemmeno un modo per usare il puntatore nin un'espressione booleana. Supponendo chetapeè solo infinito a destra, possiamo dichiarare un underflow puntatore "errore di sistema" se nmai negativo. Inoltre, la nostra lingua ha una exitdichiarazione con un argomento, per dare una risposta booleana.

Quindi il primo punto è che questo linguaggio di programmazione è un buon linguaggio di specifica per una macchina Turing. Si può facilmente vedere che, ad eccezione dell'array di nastri, il codice ha solo molti stati possibili: lo stato di tutte le sue variabili dichiarate, l'attuale linea di esecuzione e il suo stack di subroutine. Quest'ultimo ha solo una quantità limitata di stato perché le funzioni ricorsive non sono consentite. Si potrebbe immaginare un "compilatore" che crea una macchina Turing "reale" da un codice di questo tipo, ma i dettagli non sono importanti. Il punto è che abbiamo un linguaggio di programmazione con una sintassi abbastanza buona, ma tipi di dati molto primitivi.

Il resto della costruzione è di convertirlo in un linguaggio di programmazione più vivibile con un elenco finito di funzioni di libreria e fasi di precompilazione. Possiamo procedere come segue:

  1. Con un precompilatore, possiamo espandere il tipo di dati booleani in un alfabeto di simboli più grande ma finito come ASCII. Possiamo assumere che tapeassume valori in questo alfabeto più grande. Possiamo lasciare un marcatore all'inizio del nastro per impedire il underflow del puntatore, e un marcatore mobile alla fine del nastro per impedire che il TM pattini all'infinito sul nastro accidentalmente. Siamo in grado di implementare operazioni binarie arbitrarie tra simboli e conversioni in booleane ife whiledichiarazioni. (In realtà ifpuò essere implementato anche con while, se non fosse disponibile.)

  2. KKioioK

  3. Designiamo un nastro come "memoria" con valori simbolici e gli altri come "registri" o "variabili" senza segno, con valori interi. Conserviamo gli interi in binario little-endian con marcatori di terminazione. Per prima cosa implementiamo la copia di un registro e il decremento binario di un registro. Combinando questo con l'incremento e il decremento del puntatore di memoria, possiamo implementare la ricerca ad accesso casuale della memoria dei simboli. Possiamo anche scrivere funzioni per calcolare l'addizione binaria e la moltiplicazione di numeri interi. Non è difficile scrivere una funzione di aggiunta binaria con operazioni bit a bit e una funzione da moltiplicare per 2 con spostamento a sinistra. (O proprio il turno giusto, poiché è little-endian.) Con queste primitive, possiamo scrivere una funzione per moltiplicare due registri usando l'algoritmo di moltiplicazione lungo.

  4. Possiamo riorganizzare il nastro di memoria da una matrice di simboli unidimensionale symbol[n]a una matrice di simboli bidimensionali symbol[x,y]utilizzando la formula n = (x+y)*(x+y) + y. Ora possiamo usare ogni riga della memoria per esprimere un numero intero senza segno in binario con un simbolo di terminazione, per ottenere una memoria unidimensionale, ad accesso casuale, con valori interi memory[x]. Possiamo implementare la lettura dalla memoria a un registro intero e la scrittura da un registro alla memoria. Molte funzioni possono ora essere implementate con funzioni: aritmetica con segno e virgola mobile, stringhe di simboli, ecc.

  5. Solo un'altra struttura di base richiede rigorosamente un precompilatore, ovvero funzioni ricorsive. Questo può essere fatto con una tecnica ampiamente utilizzata per implementare linguaggi interpretati. Assegniamo a ogni funzione di alto livello e ricorsiva una stringa di nomi e organizziamo il codice di basso livello in un unico grande whileciclo che mantiene uno stack di chiamate con i soliti parametri: il punto di chiamata, la funzione chiamata e un elenco di argomenti.

A questo punto, la costruzione ha abbastanza caratteristiche di un linguaggio di programmazione di alto livello che ulteriori funzionalità sono più l'argomento dei linguaggi di programmazione e dei compilatori piuttosto che la teoria CS. È anche già facile scrivere un simulatore di Turing-machine in questo linguaggio sviluppato. Scrivere un autocompilatore per la lingua non è esattamente semplice, ma sicuramente standard. Naturalmente è necessario un compilatore esterno per creare la TM esterna da un codice in questo linguaggio simile a C o Python, ma ciò può essere fatto in qualsiasi linguaggio informatico.

Si noti che questa implementazione schematica supporta non solo la tesi Church-Turing dei logici per la classe di funzioni ricorsive, ma anche la tesi Church-Turing estesa (cioè polinomiale) in quanto si applica al calcolo deterministico. In altre parole, ha un sovraccarico polinomiale. In effetti, se ci viene fornita una macchina RAM o (il mio preferito) una TM a nastro d'albero, questo può essere ridotto al sovraccarico pollogaritmico per il calcolo seriale con memoria RAM.


5

Il compilatore LLVM consente di "collegare" in modo abbastanza semplice una nuova architettura. Lo chiamano scrivere un nuovo back-end e danno istruzioni dettagliate ed esempi su come farlo. Sospetto che dovrai saltare alcuni cerchi rispetto alla memoria ad accesso casuale, se non desideri indirizzare una macchina RAM Turing, ma questo è sicuramente fattibile, poiché ho visto un numero di progetti che causano la generazione di LLVM VHDL o altri linguaggi macchina molto diversi.

Ciò avrebbe l'effetto interessante di avere un compilatore di ottimizzazione all'avanguardia (per molti aspetti LLVM è più avanzato di GCC) che genera codice per una macchina Turing.


1

Non sono nella teoria del CS ma ho qualcosa che potrebbe essere utile. Ho preso un altro approch. Ho progettato un semplice processore direttamente programmabile con un piccolo sottoinsieme di C. Non esiste un codice assembly, solo un codice C-like. Puoi usare lo stesso strumento che ho usato e modificare questo processore per progettare il tuo simulatore di macchine Turing. Mi ci sono voluti 4 giorni per progettare, simulare e testare questo processore, ben poche istruzioni! Gli strumenti che ho usato mi hanno persino permesso di generare codice sintetizzabile VHDL reale. È un vero processore funzionante.

Ecco come appare un programma: Esempio di programma di assemblaggio C-Like

Ecco una foto del processore usando questi strumenti: Circuito del processore

Gli strumenti "Novakod Studio" utilizzano un linguaggio di descrizione hardware di alto livello. Ad esempio, ecco il codice del contatore del programma: psC - Esempio di codice C parallelo e sincrono basta parlare, se qualcuno è interessato, ecco le informazioni pubbliche per contattarmi: https://repertoire.uqac.ca/Fiche.aspx?id=JjstNzsH0&link=1

Luc


l'indirizzamento della memoria utilizza un numero fisso di bit per individuare gli indirizzi?
vzn

Sì, ma è semplice modificare la dimensione della memoria (int DataMemory [SIZE]. Il linguaggio supporta numeri interi di lunghezza variabile (int: 10). Ma poiché è destinato a FPGA, l'array è statico e costante di dimensione.
Luc Morin

1

Che ne dite di prendere qui l'idea rappresentata dall'utente GMB (La macchina di Turing con un nastro può simulare una macchina di Turing con N nastri intrecciando i N nastri su un singolo nastro e leggendo uno di quei nastri saltando N posizioni alla volta, un Turing la macchina con N nastri può implementare ...) e scrivere un programma di Turing machine che implementa una semplicistica RAM-machine. La RAM-machine potrebbe in realtà essere una CPU semplicistica, reale, con back-end LLVM o GCC disponibili. Quindi GCC / LLVM può essere utilizzato per la compilazione incrociata di un programma C per quella CPU e il programma Turing machine che simula la macchina RAM, esegue la simulazione della macchina RAM facendo eseguire alla macchina RAM simulata l'esecuzione dell'uscita GCC / LLVM. L'implementazione della macchina Turing potrebbe essere un codice C molto semplice che si adatta a un piccolo file C.

Per quanto riguarda la RAM-machine, esiste quindi un progetto dimostrativo, in cui una CPU a 32 bit è simulata da un microcontrollore a 8 bit e la CPU a 32 bit simulata avvia Linux. Lento da morire, ma secondo l'autore , Dmitry Grinberg, ha funzionato. Forse la CPU Zylin (zylin utente GitHub) potrebbe essere una scelta praticabile per la macchina RAM simulabile. Un altro candidato alla RA-machine potrebbe essere il dot com ProjectOberon di Niklaus Wirth .

(Il "punto" e la "com" nel mio testo sono dovuti al fatto che ho appena, 2015_10_21, registrato il mio account su cstheory.stackexchange e l'app Web non consente più di 2 collegamenti per utenti inesperti, nonostante il fatto che possono vedere automaticamente dai miei altri account stackexchange che potrei essere stupido, ma non sono un troll.)

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.