Come può compilare un compilatore stesso?


168

Sto studiando CoffeeScript sul sito http://coffeescript.org/ e ha il testo

Il compilatore CoffeeScript è esso stesso scritto in CoffeeScript

Come può compilare un compilatore stesso o cosa significa questa affermazione?


14
Un altro termine per un compilatore che può compilare se stesso è un self-hostingcompilatore. Vedi programmers.stackexchange.com/q/263651/6221
oɔɯǝɹ

37
Perché un compilatore non dovrebbe essere in grado di compilare se stesso?
user253751

48
Vi sono almeno due copie del compilatore coinvolto. Una preesistente compila una nuova copia. Il nuovo può essere o meno identico al vecchio.
bdsl,

12
Potresti anche essere interessato a Git: il suo codice sorgente viene tracciato, ovviamente, in un repository Git.
Greg d'Eon,

7
Si tratta di chiedere "Come potrebbe una stampante Xerox stampare gli schemi su se stessa?" I compilatori compilano testo in codice byte. Se il compilatore può compilare qualsiasi codice byte utilizzabile, è possibile scrivere il codice compilatore nella rispettiva lingua e quindi passare il codice attraverso il compilatore per generare l'output.
RLH,

Risposte:


219

La prima edizione di un compilatore non può essere generata automaticamente da un linguaggio di programmazione specifico; la tua confusione è comprensibile. Una versione successiva del compilatore con più funzionalità di linguaggio (con la fonte riscritta nella prima versione del nuovo linguaggio) potrebbe essere costruita dal primo compilatore. Quella versione potrebbe quindi compilare il compilatore successivo e così via. Ecco un esempio:

  1. Il primo compilatore di CoffeeScript è scritto in Ruby, producendo la versione 1 di CoffeeScript
  2. Il codice sorgente del compilatore CS viene riscritto in CoffeeScript 1
  3. Il compilatore CS originale compila il nuovo codice (scritto in CS 1) nella versione 2 del compilatore
  4. Vengono apportate modifiche al codice sorgente del compilatore per aggiungere nuove funzionalità di lingua
  5. Il secondo compilatore CS (il primo scritto in CS) compila il nuovo codice sorgente rivisto nella versione 3 del compilatore
  6. Ripetere i passaggi 4 e 5 per ogni iterazione

Nota: non sono sicuro di come siano numerate le versioni di CoffeeScript, questo è solo un esempio.

Questo processo è generalmente chiamato bootstrap . Un altro esempio di compilatore di bootstrap è rustcil compilatore per il linguaggio Rust .


5
L'altro percorso per avviare il bootstrap di un compilatore è scrivere un interprete per (un sottoinsieme) della tua lingua.
Aron,

Come ulteriore alternativa al bootstrap con un compilatore o un interprete scritto in un'altra lingua, il percorso della vecchia scuola sarebbe quello di assemblare a mano la fonte del compilatore. Chuck Moore spiega come eseguire questa operazione per un interprete Forth nel capitolo 9, "Programmi che si avviano", al termine della programmazione di un linguaggio orientato ai problemi ( web.archive.org/web/20160327044521/www.colorforth.com/POL .htm ), basato sul fatto di averlo fatto due volte prima a mano. Qui l'immissione del codice avviene tramite un pannello frontale che consente la memorizzazione diretta dei valori negli indirizzi di memoria controllati da interruttori a levetta per bit.
Jeremy W. Sherman,

59

Nel documento Reflections on Trusting Trust , Ken Thompson, uno dei creatori di Unix, scrive un'affascinante (e facilmente leggibile) panoramica di come si compila il compilatore C. Concetti simili possono essere applicati a CoffeeScript o a qualsiasi altra lingua.

L'idea di un compilatore che compila il proprio codice è vagamente simile a un quine : codice sorgente che, quando eseguito, produce come output il codice sorgente originale. Ecco un esempio di quine CoffeeScript. Thompson ha dato questo esempio di C quine:

char s[] = {
    '\t',
    '0',
    '\n',
    '}',
    ';',
    '\n',
    '\n',
    '/',
    '*',
    '\n',
    … 213 lines omitted …
    0
};

/*
 * The string s is a representation of the body
 * of this program from '0'
 * to the end.
 */

main()
{
    int i;

    printf("char\ts[] = {\n");
    for(i = 0; s[i]; i++)
        printf("\t%d,\n", s[i]);
    printf("%s", s);
}

Quindi, potresti chiederti come viene insegnato al compilatore che una sequenza di escape come '\n'rappresenta il codice ASCII 10. La risposta è che da qualche parte nel compilatore C, c'è una routine che interpreta i caratteri letterali, contenente alcune condizioni come questa per riconoscere le sequenze di barra rovesciata:

…
c = next();
if (c != '\\') return c;        /* A normal character */
c = next();
if (c == '\\') return '\\';     /* Two backslashes in the code means one backslash */
if (c == 'r')  return '\r';     /* '\r' is a carriage return */
…

Quindi, possiamo aggiungere una condizione al codice sopra ...

if (c == 'n')  return 10;       /* '\n' is a newline */

... per produrre un compilatore che sappia che '\n'rappresenta ASCII 10. È interessante notare che quel compilatore e tutti i successivi compilatori da esso compilati "conoscono" tale mappatura, quindi nella prossima generazione del codice sorgente, è possibile modificare quest'ultima riga in

if (c == 'n')  return '\n';

... e farà la cosa giusta! La 10deriva dal compilatore, e non ha più bisogno di essere esplicitamente definito nel codice sorgente del compilatore. 1

Questo è un esempio di una funzionalità del linguaggio C implementata nel codice C. Ora, ripeti questo processo per ogni singola lingua e hai un compilatore "self-hosting": un compilatore C scritto in C.


1 La svolta della trama descritta nel documento è che dal momento che al compilatore possono essere "insegnati" fatti come questo, può anche essere insegnato male a generare eseguibili trojan in un modo che è difficile da rilevare, e un tale atto di sabotaggio può persistere in tutti i compilatori prodotti dal compilatore contaminato.


7
Sebbene si tratti di un'informazione interessante, non credo che risponda alla domanda. I tuoi esempi presuppongono che tu abbia già un compilatore bootstrap, oppure in quale lingua è scritto il compilatore C?
Arturo Torres Sánchez,

9
@ ArturoTorresSánchez Spiegazioni diverse funzionano bene per persone diverse. Non intendo ribadire ciò che è stato detto in altre risposte. Piuttosto, trovo che le altre risposte parlino a un livello superiore rispetto a come mi piace pensare. Personalmente preferisco un'illustrazione concreta di come viene aggiunta una singola funzione e lasciare che il lettore estrapoli da quella, invece di una visione d'insieme.
200_successo

5
OK, capisco la tua prospettiva. È solo che la domanda è più "come può compilare un compilatore se il compilatore per compilare il compilatore non esiste" e meno "come aggiungere nuove funzionalità a un compilatore con bootstrap".
Arturo Torres Sánchez,

17
La domanda in sé è ambigua e aperta. Sembra che alcune persone lo interpretino nel senso che "come può un compilatore CoffeeScript compilarsi?". La risposta irriverente, come indicato in un commento, è "perché non dovrebbe essere in grado di compilare se stesso, proprio come compila un codice?" Interpreto nel senso "come può nascere un compilatore self-hosting?", E ho fornito un'illustrazione di come un compilatore può essere insegnato su una delle sue caratteristiche linguistiche. Risponde alla domanda in modo diverso, fornendo un'illustrazione di basso livello su come viene implementata.
200_successo

1
@ ArturoTorresSánchez: "[I] n in quale lingua è scritto il compilatore C?" Molto tempo fa ho mantenuto il compilatore C originale annotato nella vecchia appendice K&R (quella per IBM 360.) Molte persone sanno che prima c'era BCPL, poi B, e che C era una versione migliorata di B. In effetti, c'erano molti parti di quel vecchio compilatore che erano ancora scritte in B e che non erano mai state riscritte in C. Le variabili avevano la forma di una singola lettera / cifra, non si supponeva che l'aritmetica del puntatore fosse ridimensionata automaticamente, ecc. Quel vecchio codice testimoniava il bootstrap da B a C. Il primo compilatore "C" è stato scritto in B.
Eliyahu Skoczylas il

29

Hai già ottenuto un'ottima risposta, tuttavia voglio offrirti una prospettiva diversa, che si spera ti sia illuminante. Stabiliamo innanzitutto due fatti su cui possiamo entrambi concordare:

  1. Il compilatore CoffeeScript è un programma in grado di compilare programmi scritti in CoffeeScript.
  2. Il compilatore CoffeeScript è un programma scritto in CoffeeScript.

Sono sicuro che puoi essere d'accordo sul fatto che sia il numero 1 che il numero 2 siano veri. Ora, guarda le due affermazioni. Vedete ora che è del tutto normale che il compilatore CoffeeScript sia in grado di compilare il compilatore CoffeeScript?

Al compilatore non importa cosa compila. Finché è un programma scritto in CoffeeScript, può compilarlo. E il compilatore CoffeeScript stesso sembra essere un tale programma. Al compilatore CoffeeScript non importa che sia il compilatore CoffeeScript stesso che sta compilando. Tutto ciò che vede è un po 'di codice CoffeeScript. Periodo.

Come può compilare un compilatore stesso o cosa significa questa affermazione?

Sì, questo è esattamente ciò che significa questa affermazione, e spero che tu possa vedere ora come questa affermazione è vera.


2
Non so molto sulla sceneggiatura del caffè, ma potresti chiarire il punto 2 affermando che è STATO scritto nella sceneggiatura del caffè ma è stato compilato e quindi è un codice macchina. E comunque, potresti per favore spiegare il problema del pollo e delle uova allora. Se il compilatore è stato scritto in una lingua per la quale non è stato ancora scritto un compilatore, come può essere eseguito o compilato?
barlop

6
La tua dichiarazione 2 è incompleta / inesatta e molto fuorviante. poiché, come dice la prima risposta, la prima non è stata scritta nella sceneggiatura del caffè. Questo è così rilevante per la sua domanda. E come "Come può compilare un compilatore stesso, o cosa significa questa affermazione?" Dici "Sì" suppongo di sì (anche se la mia mente è un po 'piccola), vedo che è usato per compilare versioni precedenti di se stesso, piuttosto che se stesso. Ma viene utilizzato anche per compilare se stesso? Pensavo che sarebbe stato inutile.
barlop

2
@barlop: modifica la frase 2 in " Oggi , il compilatore CoffeeScript è un programma scritto in CoffeeScript." Questo ti aiuta a capirlo meglio? Un compilatore è "solo" un programma che traduce un input (codice) in un output (programma). Quindi, se si dispone di un compilatore per il linguaggio Foo, quindi scrivere il codice sorgente per un compilatore Foo nella lingua Foo stessa e alimentare tale sorgente al primo compilatore Foo, si ottiene un secondo compilatore Foo come output. Questo fatto da molte lingue (ad esempio, tutti i compilatori C che conosco sono scritti in ... C).
DarkDust,

3
Il compilatore non può compilare se stesso. Il file di output non è la stessa istanza del compilatore che produce il file di output. Spero che tu possa vedere ora come questa affermazione sia falsa.
pabrams,

3
@pabrams Perché lo pensi? L'output potrebbe essere identico al compilatore utilizzato per produrlo. Ad esempio, se compilo GCC 6.1 con GCC 6.1, ottengo una versione di GCC 6.1 compilata con GCC 6.1. E poi se lo uso per compilare GCC 6.1, ottengo anche una versione di GCC 6.1 compilata con GCC 6.1, che dovrebbe essere identica (ignorando cose come i timestamp).
user253751,

9

Come può compilare un compilatore stesso o cosa significa questa affermazione?

Significa esattamente questo. Prima di tutto, alcune cose da considerare. Ci sono quattro oggetti che dobbiamo guardare:

  • Il codice sorgente di qualsiasi programma CoffeScript arbitrario
  • L'assemblaggio (generato) di qualsiasi programma CoffeScript arbitrario
  • Il codice sorgente del compilatore CoffeScript
  • L'assembly (generato) del compilatore CoffeScript

Ora, dovrebbe essere ovvio che è possibile utilizzare l'assembly generato - l'eseguibile - del compilatore CoffeScript per compilare qualsiasi programma CoffeScript arbitrario e generare l'assembly per quel programma.

Ora, il compilatore CoffeScript stesso è solo un programma CoffeScript arbitrario, e quindi può essere compilato dal compilatore CoffeScript.

Sembra che la tua confusione derivi dal fatto che quando crei la tua nuova lingua, non hai ancora un compilatore che puoi usare per compilare il tuo compilatore. Questo sembra sicuramente un problema con le uova di gallina , giusto?

Introdurre il processo chiamato bootstrap .

  1. Scrivi un compilatore in una lingua già esistente (nel caso di CoffeScript, il compilatore originale è stato scritto in Ruby) che può compilare un sottoinsieme della nuova lingua
  2. Scrivi un compilatore che può compilare un sottoinsieme della nuova lingua nella nuova lingua stessa. Puoi utilizzare solo le funzionalità della lingua che il compilatore può eseguire dal passaggio precedente.
  3. Si utilizza il compilatore dal passaggio 1 per compilare il compilatore dal passaggio 2. Ciò lascia un assembly originariamente scritto in un sottoinsieme della nuova lingua e che è in grado di compilare un sottoinsieme della nuova lingua.

Ora devi aggiungere nuove funzionalità. Supponiamo che tu abbia implementato solo while-loops, ma desideri anche for-loops. Questo non è un problema, poiché puoi riscrivere qualsiasi for-loop in modo tale che sia while-loop. Questo significa che puoi usare solo while-loops nel codice sorgente del tuo compilatore, poiché l'assembly che hai a portata di mano può solo compilare quelli. Ma puoi creare funzioni all'interno del tuo compilatore in grado di compilare e compilare for-loops con esso. Quindi si utilizza l'assembly già in uso e si compila la nuova versione del compilatore. E ora hai un assembly di un compilatore che può anche analizzare e compilare for-loops! Ora puoi tornare al file sorgente del tuo compilatore e riscrivere tutti i whileloop che non vuoi forinserire in -loops.

Risciacquare e ripetere fino a quando tutte le funzionalità di lingua desiderate possono essere compilate con il compilatore.

whilee forovviamente erano solo esempi, ma questo funziona per qualsiasi nuova funzione linguistica che desideri. E poi ti trovi nella situazione in cui si trova CoffeScript: il compilatore si compila da solo.

C'è molta letteratura là fuori. Riflessioni sulla fiducia La fiducia è un classico che tutti gli interessati a quell'argomento dovrebbero leggere almeno una volta.


5
(La frase "Il compilatore CoffeeScript è esso stesso scritto in CoffeeScript", è vera, ma "Un compilatore può compilare se stesso" è falso.)
pabrams,

4
No, è completamente vero. Il compilatore può compilare se stesso. Non ha senso. Supponi di avere l'eseguibile in grado di compilare la versione X della lingua. Scrivi un compilatore che può compilare la versione X + 1 e compilarlo con il compilatore che hai (che è la versione X). Si finisce con un eseguibile che può compilare la versione X + 1 della lingua. Ora puoi andare e usare quel nuovo eseguibile per ricompilare il compilatore. Ma a che fine? È già avete l'eseguibile che fa quello che si vuole. Il compilatore può compilare qualsiasi programma valido, quindi può compilare completamente se stesso!
Polygnome,

1
In effetti non è inaudito costruire un paio di volte, mentre il moderno Freepascal costruisce il compilatore per un totale di 5 volte.
lavaggio:

1
@pabrams Scrivere "Non toccare" e "Oggetto caldo. Non toccare" non fa differenza per il messaggio previsto della frase. Finché il pubblico previsto del messaggio (programmatori) capisce il messaggio previsto della frase (una build del compilatore può compilare la sua fonte) indipendentemente da come è scritta, questa discussione è inutile. Allo stato attuale, l'argomento non è valido. A meno che tu non sia in grado di dimostrare che il pubblico previsto del messaggio è non programmatore, allora, e solo allora, hai ragione.
DarkDestry,

2
"Good English" di @pabrams è l'inglese che comunica chiaramente le idee al pubblico previsto e nel modo previsto dallo scrittore o dal relatore. Se il pubblico previsto sono programmatori e i programmatori lo capiscono, è un buon inglese. Dire "La luce esiste sia come particelle che come onde" è fondamentalmente equivalente a "La luce esiste sia come fotoni che come onde elettromagnetiche". Per un fisico, significano letteralmente la stessa cosa. Ciò significa che dovremmo sempre usare la frase più lunga e più chiara? No! Perché complica la lettura quando il significato è già chiaro al pubblico previsto.
DarkDestry,

7

Un piccolo ma importante chiarimento

Qui il termine compilatore riflette sul fatto che ci sono due file coinvolti. Uno è un eseguibile che accetta come file di input scritti in CoffeScript e produce come file di output un altro eseguibile, un file oggetto collegabile o una libreria condivisa. L'altro è un file sorgente di CoffeeScript che descrive semplicemente la procedura per la compilazione di CoffeeScript.

Si applica il primo file al secondo, producendo un terzo che è in grado di eseguire lo stesso atto di compilazione del primo (possibilmente di più, se il secondo file definisce funzionalità non implementate dal primo), e quindi si può sostituire il primo se si così desiderio.


4
  1. Il compilatore CoffeeScript è stato scritto per la prima volta in Ruby.
  2. Il compilatore CoffeeScript è stato quindi riscritto in CoffeeScript.

Poiché la versione Ruby del compilatore CoffeeScript esisteva già, è stata utilizzata per creare la versione CoffeeScript del compilatore CoffeeScript.

inserisci qui la descrizione dell'immagine Questo è noto come compilatore self-hosting .

È estremamente comune e di solito deriva dal desiderio di un autore di usare la propria lingua per mantenere la crescita di quella lingua.


3

Non è una questione di compilatori qui, ma una questione di espressività del linguaggio, dal momento che un compilatore è solo un programma scritto in una lingua.

Quando diciamo che "una lingua è scritta / implementata" intendiamo effettivamente che un compilatore o un interprete per quella lingua è implementato. Esistono linguaggi di programmazione in cui è possibile scrivere programmi che implementano la lingua (sono compilatori / interpreti per la stessa lingua). Queste lingue sono chiamate lingue universali .

Per poterlo capire, pensa a un tornio di metallo. È uno strumento utilizzato per modellare il metallo. È possibile, usando solo quello strumento, creare un altro strumento identico, creando le sue parti. Pertanto, tale strumento è una macchina universale. Naturalmente, il primo è stato creato utilizzando altri mezzi (altri strumenti) ed era probabilmente di qualità inferiore. Ma il primo è stato usato per costruirne di nuovi con maggiore precisione.

Una stampante 3D è quasi una macchina universale. È possibile stampare l'intera stampante 3D utilizzando una stampante 3D (non è possibile creare la punta che scioglie la plastica).


Mi piace l'analogia del tornio. A differenza dell'analogia del tornio, tuttavia, le imperfezioni nella prima iterazione del compilatore vengono trasmesse a tutti i compilatori successivi. Ad esempio, una risposta sopra menziona l'aggiunta di una funzione for-loop in cui il compilatore originale utilizza solo mentre loop. L'output comprende i loop for, ma l'implementazione avviene con i loop while. Se l'implementazione del ciclo while originale è imperfetta o inefficiente, lo sarà sempre!

@ Physics-Compute che è semplicemente sbagliato. In assenza di difetti di malizia di solito non si propagano durante la compilazione di un compilatore.
lavaggio:

Le traduzioni di assembly certamente passano dall'iterazione all'iterazione fino a quando la traduzione dell'assembly non viene risolta. Le nuove funzionalità che sviluppano le vecchie funzionalità non cambiano l'implementazione sottostante. Pensaci un po '.

@plugwash Vedi "Reflections on Trusting Trust" di Ken Thompson - ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf

3

Prova per induzione

Passo induttivo

La n + 1a versione del compilatore è scritta in X.

Quindi può essere compilato dall'ennesima versione del compilatore (anch'essa scritta in X).

Caso base

Ma la prima versione del compilatore scritta in X deve essere compilata da un compilatore per X scritto in una lingua diversa da X. Questo passaggio è chiamato bootstrap del compilatore.


1
Il primissimo compilatore del compilatore per il linguaggio X può essere facilmente scritto in X. In che modo è possibile interpretare questo primo compilatore . (Da un interprete X scritto in una lingua diversa da X).
Kaz,

0

I compilatori prendono una specifica di alto livello e la trasformano in un'implementazione di basso livello, come può essere eseguita su hardware. Pertanto non esiste alcuna relazione tra il formato della specifica e l'esecuzione effettiva oltre alla semantica del linguaggio preso di mira.

I compilatori incrociati si spostano da un sistema a un altro, i compilatori multilingua compilano una specifica di lingua in un'altra specifica di lingua.

Fondamentalmente la compilazione è una traduzione giusta, e il livello è generalmente di livello più alto della lingua a un livello più basso della lingua, ma ci sono molte varianti.

I compilatori di bootstrap sono i più confusi, ovviamente, perché compilano la lingua in cui sono scritti. Non dimenticare il passaggio iniziale nel bootstrap che richiede almeno una versione minima esistente eseguibile. Molti compilatori con bootstrap lavorano prima sulle funzionalità minime di un linguaggio di programmazione e aggiungono funzionalità linguistiche complesse aggiuntive a condizione che la nuova funzionalità possa essere espressa utilizzando le funzionalità precedenti. Se così non fosse, richiederebbe che quella parte del "compilatore" sia stata precedentemente sviluppata in un'altra lingua.

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.