Scrivere un compilatore nella sua lingua


204

Intuitivamente, sembrerebbe che un compilatore per il linguaggio Foonon possa essere scritto da solo in Foo. Più specificamente, il primo compilatore per la lingua Foonon può essere scritto in Foo, ma è possibile scrivere per qualsiasi compilatore successivo Foo.

Ma è davvero vero? Ho un vago ricordo della lettura di una lingua il cui primo compilatore è stato scritto in "se stesso". È possibile, e se sì, come?



Questa è una domanda molto antica, ma dire che ho scritto un interprete per il linguaggio Foo in Java. Poi con il foo della lingua, ho scritto il suo interprete. Foo avrebbe comunque bisogno del JRE giusto?
George Xavier,

Risposte:


231

Questo si chiama "bootstrap". Devi prima creare un compilatore (o interprete) per la tua lingua in un'altra lingua (di solito Java o C). Fatto ciò, puoi scrivere una nuova versione del compilatore in lingua Foo. Si utilizza il primo compilatore bootstrap per compilare il compilatore e quindi si utilizza questo compilatore compilato per compilare tutto il resto (comprese le versioni future di se stesso).

La maggior parte delle lingue sono effettivamente create in questo modo, in parte perché ai progettisti di lingue piace usare la lingua che stanno creando, e anche perché un compilatore non banale spesso serve da utile punto di riferimento per quanto "completa" possa essere la lingua.

Un esempio di questo sarebbe Scala. Il suo primo compilatore è stato creato in Pizza, un linguaggio sperimentale di Martin Odersky. A partire dalla versione 2.0, il compilatore è stato completamente riscritto in Scala. Da quel momento in poi, il vecchio compilatore Pizza potrebbe essere completamente scartato, a causa del fatto che il nuovo compilatore Scala poteva essere utilizzato per compilare se stesso per future iterazioni.


Forse una domanda stupida: se vuoi trasferire il tuo compilatore su un'altra architettura di microprocessore, il bootstrap dovrebbe ricominciare da un compilatore funzionante per quell'architettura. È giusto? Se questo è corretto, ciò significa che è meglio mantenere il primo compilatore in quanto potrebbe essere utile trasferire il compilatore su altre architetture (specialmente se è scritto in un "linguaggio universale" come C)?
piertoni,

2
@piertoni in genere sarebbe più semplice ripetere il retarget del backend del compilatore sul nuovo microprocessore.
bstpierre,

Usa LLVM come backend, ad esempio

76

Mi ricordo l'ascolto di un podcast di Ingegneria del Software Radio in cui Dick Gabriel ha parlato bootstrapping l'interprete LISP originale scrivendo una versione ridotta all'osso in LISP su carta e la mano assemblando in codice macchina. Da quel momento in poi, le altre funzionalità di LISP sono state sia scritte che interpretate con LISP.


Tutto viene avviato da un transistor di genesi con molte mani

47

Aggiungendo una curiosità alle risposte precedenti.

Ecco una citazione dal manuale Linux From Scratch , nel passaggio in cui si inizia a costruire il compilatore GCC dal suo sorgente. (Linux From Scratch è un modo per installare Linux che è radicalmente diverso dall'installazione di una distribuzione, in quanto devi compilare davvero ogni singolo binario del sistema di destinazione.)

make bootstrap

Il target 'bootstrap' non solo compila GCC, ma lo compila più volte. Usa i programmi compilati in un primo round per compilare se stesso una seconda volta, e poi di nuovo una terza volta. Quindi confronta queste seconde e terze compilazioni per assicurarsi che possa riprodursi in modo impeccabile. Ciò implica anche che è stato compilato correttamente.

Tale utilizzo del target "bootstrap" è motivato dal fatto che il compilatore utilizzato per creare la toolchain del sistema di destinazione potrebbe non avere la stessa versione del compilatore di destinazione. Procedendo in questo modo si ottiene sicuramente, nel sistema di destinazione, un compilatore in grado di compilare se stesso.


12
"devi compilare davvero ogni singolo binario del sistema di destinazione" e tuttavia devi iniziare con un binario gcc che hai ottenuto da qualche parte, perché il sorgente non può compilarsi da solo. Mi chiedo se tu risalissi al lignaggio di ogni binario gcc che è stato usato per ricompilare ogni gcc successivo, torneresti indietro al compilatore C originale di K&R?
robru,

43

Quando scrivi il tuo primo compilatore per C, lo scrivi in ​​un'altra lingua. Ora, hai un compilatore per C in, diciamo, assemblatore. Alla fine, verrai nel posto in cui devi analizzare le stringhe, in particolare sfuggire alle sequenze. Scriverai il codice per convertirlo \nnel carattere con il codice decimale 10 (e \rin 13, ecc.).

Dopo che il compilatore è pronto, inizierai a reimplementarlo in C. Questo processo si chiama " bootstrap " ".

Il codice di analisi delle stringhe diventerà:

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

Quando questo viene compilato, hai un binario che capisce '\ n'. Ciò significa che è possibile modificare il codice sorgente:

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

Quindi dov'è l'informazione che '\ n' è il codice per 13? È nel binario! È come il DNA: la compilazione del codice sorgente C con questo file binario erediterà queste informazioni. Se il compilatore si compila da solo, passerà questa conoscenza alla sua prole. Da questo punto in poi, non c'è modo di vedere dalla fonte da solo cosa farà il compilatore.

Se vuoi nascondere un virus nella fonte di alcuni programmi, puoi farlo in questo modo: Ottieni la fonte di un compilatore, trova la funzione che compila le funzioni e sostituiscila con questa:

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

Le parti interessanti sono A e B. A è il codice sorgente di compileFunction includere il virus, probabilmente crittografato in qualche modo, quindi non è ovvio dalla ricerca del binario risultante. Questo assicura che la compilazione con il compilatore stesso conserverà il codice di iniezione del virus.

B è lo stesso per la funzione che vogliamo sostituire con il nostro virus. Ad esempio, potrebbe essere la funzione "login" nel file sorgente "login.c" che probabilmente proviene dal kernel Linux. Potremmo sostituirlo con una versione che accetterà la password "joshua" per l'account root oltre alla normale password.

Se lo compili e lo diffondi come binario, non ci sarà modo di trovare il virus guardando la fonte.

La fonte originale dell'idea: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/


1
Qual è lo scopo della seconda metà sullo scrivere compilatori infestati da virus? :)
mhvelplund,

3
@mhvelplund Basta diffondere la conoscenza di come il bootstrap possa ucciderti.
Aaron Digulla,

19

Non puoi scrivere un compilatore in sé perché non hai nulla con cui compilare il tuo codice sorgente iniziale. Ci sono due approcci per risolvere questo.

Il meno favorito è il seguente. Scrivi un compilatore minimo in assemblatore (yuck) per un set minimo della lingua e quindi usi quel compilatore per implementare funzionalità extra della lingua. Costruisci la strada fino a quando non hai un compilatore con tutte le funzionalità linguistiche per se stesso. Un processo doloroso che di solito viene fatto solo quando non hai altra scelta.

L'approccio preferito è usare un compilatore incrociato. Si modifica il back-end di un compilatore esistente su una macchina diversa per creare output in esecuzione sulla macchina di destinazione. Quindi hai un bel compilatore completo attivo e funzionante sulla macchina target. Il più popolare per questo è il linguaggio C, poiché ci sono molti compilatori esistenti che hanno back-end collegabili che possono essere scambiati.

Un fatto poco noto è che il compilatore GNU C ++ ha un'implementazione che utilizza solo il sottoinsieme C. Il motivo è che di solito è facile trovare un compilatore C per un nuovo computer di destinazione che consente di compilare da esso l'intero compilatore GNU C ++. Ora hai avviato te stesso con un compilatore C ++ sul computer di destinazione.


14

Generalmente, devi prima avere un taglio funzionante (se primitivo) del compilatore, quindi puoi iniziare a pensare di renderlo self-hosting. Questo è in realtà considerato una pietra miliare importante in alcuni linguaggi.

Da quello che ricordo di "mono", è probabile che dovranno aggiungere alcune cose alla riflessione per farlo funzionare: il team mono continua a sottolineare che alcune cose semplicemente non sono possibili con Reflection.Emit ; ovviamente, il team MS potrebbe dimostrarli sbagliati.

Questo ha alcuni vantaggi reali : è un test unitario abbastanza buono, per cominciare! E hai solo un linguaggio di cui preoccuparti (cioè è possibile che un esperto di C # non conosca molto C ++; ma ora puoi correggere il compilatore C #). Ma mi chiedo se non v'è una quantità di orgoglio professionale al lavoro qui: semplicemente vogliono che sia self-hosting.

Non proprio un compilatore, ma di recente ho lavorato su un sistema che è self hosting; il generatore di codice viene utilizzato per generare il generatore di codice ... quindi se lo schema cambia lo eseguo semplicemente su se stesso: nuova versione. Se c'è un bug, torno a una versione precedente e riprovo. Molto conveniente e molto facile da mantenere.


Aggiornamento 1

Ho appena visto questo video di Anders al PDC e (circa un'ora dopo) fornisce alcune ragioni molto più valide - tutto sul compilatore come servizio. Solo per la cronaca.


4

Ecco un dump (argomento difficile su cui cercare, in realtà):

Questa è anche l'idea di PyPy e Rubinius :

(Penso che questo potrebbe valere anche per Forth , ma non so nulla di Forth.)


Il primo link a un articolo apparentemente correlato a Smalltalk punta attualmente a una pagina senza apparenti informazioni utili e immediate.
nbro,

1

GNAT, il compilatore Ada GNU, richiede che un compilatore Ada sia completamente compilato. Questo può essere un problema quando si esegue il porting su una piattaforma in cui non è disponibile alcun binario GNAT.


1
Non vedo perché? Non c'è alcuna regola che devi avviare più di una volta (come per ogni nuova piattaforma), puoi anche eseguire la compilazione incrociata con una corrente.
Marco van de Voort,

1

In realtà, la maggior parte dei compilatori sono scritti nella lingua che compilano, per i motivi sopra indicati.

Il primo compilatore bootstrap è generalmente scritto in C, C ++ o Assembly.


1

Il compilatore C # del progetto Mono è "self-hosted" da molto tempo, ciò che significa è che è stato scritto in C # stesso.

Quello che so è che il compilatore è stato avviato come puro codice C, ma una volta implementate le funzionalità "base" di ECMA hanno iniziato a riscrivere il compilatore in C #.

Non sono a conoscenza dei vantaggi di scrivere il compilatore nella stessa lingua, ma sono sicuro che abbia a che fare almeno con le funzionalità che il linguaggio stesso può offrire (C, ad esempio, non supporta la programmazione orientata agli oggetti) .

Puoi trovare maggiori informazioni qui .


1

Ho scritto SLIC (System of Languages ​​for Implementing Compilers) in sé. Quindi compilato a mano in assembly. SLIC ha molto in quanto era un singolo compilatore di cinque sotto-lingue:

  • Linguaggio di programmazione parser SYNTAX PPL
  • Linguaggio di generazione del codice PSEUDO a scansione di alberi basato su GENERATOR LISP 2
  • ISO in sequenza, codice PSEUDO, linguaggio di ottimizzazione
  • Macro PSEUDO come codice assembly che produce linguaggio.
  • MACHOP Istruzioni di assemblaggio-macchina che definiscono la lingua.

SLIC è stato ispirato da CWIC (compilatore per la scrittura e l'implementazione di compilatori). A differenza della maggior parte dei pacchetti di sviluppo del compilatore, SLIC e CWIC hanno affrontato la generazione di codice con linguaggi specializzati, specifici per il dominio. SLIC estende la generazione del codice CWIC aggiungendo le sotto-lingue ISO, PSEUDO e MACHOP che separano le specifiche della macchina target dal linguaggio del generatore di scansione degli alberi.

LISP 2 alberi ed elenchi

Il sistema di gestione dinamica della memoria del linguaggio generatore basato su LISP 2 è un componente chiave. Gli elenchi sono espressi nella lingua racchiusa tra parentesi quadre, i suoi componenti sono separati da virgole, ovvero un elenco di tre elementi [a, b, c].

Alberi:

     ADD
    /   \
  MPY     3
 /   \
5     x

sono rappresentati da liste la cui prima voce è un oggetto nodo:

[ADD,[MPY,5,x],3]

Gli alberi sono comunemente visualizzati con il nodo separato che precede i rami:

ADD[MPY[5,x],3]

Annullamento dell'analisi con le funzioni del generatore basate su LISP 2

Una funzione del generatore è un insieme denominato di (unparse) => azione> coppie ...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

Le espressioni non analisi sono test che abbinano i modelli di alberi e / o i tipi di oggetti suddividendoli e assegnando tali parti alla variabile locale da elaborare mediante la sua azione procedurale. Un po 'come una funzione sovraccarica che prende diversi tipi di argomenti. Tranne i test () => ... vengono tentati nell'ordine codificato. Il primo riuscito sparisce eseguendo la sua azione corrispondente. Le espressioni unparse sono test di smontaggio. ADD [x, y] corrisponde a un albero ADD a due rami che assegna i suoi rami alle variabili locali x e y. L'azione può essere un'espressione semplice o un blocco di codice limitato .BEGIN ... .END. Oggi userei i blocchi di stile {...}. La corrispondenza dell'albero, [], le regole unparse possono chiamare i generatori che passano i risultati restituiti all'azione:

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

In particolare il precedente expr_gen unparse corrisponde a un albero ADD a due rami. All'interno del modello di test verrà chiamato un singolo generatore di argomenti posizionato in un ramo di un albero con quel ramo. Il suo elenco di argomenti è costituito da variabili locali assegnate a oggetti restituiti. Al di sopra di unparse si specifica che un ramo è lo smontaggio dell'albero ADD, ricorsivo premendo ciascun ramo su expr_gen. Il ritorno del ramo sinistro inserito nelle variabili locali x. Allo stesso modo il ramo destro è passato a expr_gen con y l'oggetto return. Quanto sopra potrebbe far parte di un valutatore di espressioni numeriche. C'erano funzioni di scelta rapida chiamate vettori in cui sopra invece della stringa di nodo si poteva usare un vettore di nodi con un vettore di azioni corrispondenti:

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

Il valutatore di espressioni più completo sopra riportato assegna il ritorno dal ramo sinistro expr_gen a xe il ramo destro a y. Il vettore di azione corrispondente eseguito su xey restituito. Le ultime coppie azione =parse => corrispondono agli oggetti numerici e simbolici.

Simbolo e attributi simbolo

I simboli possono avere attributi con nome. val: (x) accede all'attributo val dell'oggetto simbolo contenuto in x. Una pila di tabelle di simboli generalizzata fa parte di SLIC. La tabella SIMBOLI può essere spinta e visualizzata fornendo simboli locali per le funzioni. I simboli appena creati sono catalogati nella tabella dei simboli in alto. La ricerca dei simboli cerca nello stack della tabella dei simboli dalla tabella superiore prima all'indietro nello stack.

Generazione di codice indipendente dalla macchina

Il linguaggio generatore di SLIC produce oggetti istruzione PSEUDO, aggiungendoli a un elenco di codici di sezioni. A .FLUSH fa eseguire il suo elenco di codici PSEUDO rimuovendo ogni istruzione PSEUDO dall'elenco e chiamandolo. Dopo l'esecuzione viene rilasciata una memoria degli oggetti PSEUDO. Gli organi procedurali delle azioni di PSEUDO e GENERATOR sono sostanzialmente la stessa lingua, tranne per il loro output. PSEUDO è pensato per fungere da macro di assemblaggio fornendo sequenzializzazione del codice indipendente dalla macchina. Forniscono una separazione della macchina target specifica dal linguaggio del generatore di scansione degli alberi. I PSEUDO chiamano le funzioni MACHOP per emettere il codice macchina. I MACHOP sono usati per definire pseudo-operazioni di assemblaggio (come dc, definire costanti ecc.) E istruzioni macchina o una famiglia di istruzioni simili formate usando l'immissione vettoriale. Trasformano semplicemente i loro parametri in una sequenza di campi di bit che compongono l'istruzione. Le chiamate MACHOP hanno lo scopo di assomigliare a assembly e fornire la formattazione di stampa dei campi per quando assembly viene mostrato nell'elenco di compilazione. Nel codice di esempio sto usando commenti in stile c che potrebbero essere facilmente aggiunti ma non nelle lingue originali. I MACHOP producono codice in una memoria indirizzabile a bit. Il linker SLIC gestisce l'output del compilatore. Un MACHOP per le istruzioni della modalità utente del DEC-10 usando una voce vettoriale: I MACHOP producono codice in una memoria indirizzabile a bit. Il linker SLIC gestisce l'output del compilatore. Un MACHOP per le istruzioni della modalità utente del DEC-10 usando una voce vettoriale: I MACHOP producono codice in una memoria indirizzabile a bit. Il linker SLIC gestisce l'output del compilatore. Un MACHOP per le istruzioni della modalità utente del DEC-10 usando una voce vettoriale:

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

Il .MORG 36, O (18): $ / 36; allinea la posizione a un limite di 36 bit stampando l'indirizzo $ / 36 word della posizione di 18 bit in ottale. L'opcd a 9 bit, il registro a 4 bit, il bit indiretto e il registro indice a 4 bit sono combinati e stampati come se fosse un singolo campo a 18 bit. L'indirizzo a 18 bit / 36 o il valore immediato viene emesso e stampato in ottale. Un esempio MOVEI stampato con r1 = 1 e r2 = 2:

400020 201082 000005            MOVEI r1,5(r2)

Con l'opzione assembly del compilatore si ottiene il codice assembly generato nell'elenco di compilazione.

Collegalo insieme

Il linker SLIC viene fornito come una libreria che gestisce le risoluzioni di collegamento e dei simboli. La formattazione del file di caricamento dell'output specifico di destinazione tuttavia deve essere scritta per i computer di destinazione e collegata alla libreria della libreria del linker.

Il linguaggio del generatore è in grado di scrivere alberi in un file e leggerli consentendo l'implementazione di un compilatore multipass.

Breve riepilogo della generazione e delle origini del codice

Ho esaminato prima la generazione del codice per assicurarmi che SLIC fosse un vero compilatore di compilatori. SLIC è stato ispirato da CWIC (compilatore per la scrittura e l'implementazione di compilatori) sviluppato presso la Systems Development Corporation alla fine degli anni '60. CWIC aveva solo i linguaggi SYNTAX e GENERATOR che producevano codice byte numerico fuori dal linguaggio GENERATOR. Il codice byte è stato inserito o inserito (il termine utilizzato nella documentazione CWIC) nei buffer di memoria associati alle sezioni denominate e scritti da un'istruzione .FLUSH. Un documento ACM su CWIC è disponibile negli archivi ACM.

Implementare con successo un importante linguaggio di programmazione

Alla fine degli anni '70, SLIC è stato utilizzato per scrivere un cross-compilatore COBOL. Completato in circa 3 mesi principalmente da un singolo programmatore. Ho lavorato un po 'con il programmatore, se necessario. Un altro programmatore ha scritto la libreria di runtime e i MACHOP per il target TI-990 mini-COMPUTER. Quel compilatore COBOL ha compilato sostanzialmente più righe al secondo rispetto al compilatore COBOL nativo DEC-10 scritto in assembly.

Più di un compilatore poi di solito parlava

Una grande parte della scrittura di un compilatore da zero è la libreria di runtime. Hai bisogno di una tabella dei simboli. Hai bisogno di input e output. Gestione dinamica della memoria ecc. Può essere più semplice scrivere la libreria di runtime per un compilatore e poi scrivere il compilatore. Ma con SLIC quella libreria di runtime è comune a tutti i compilatori sviluppati in SLIC. Nota: ci sono due librerie di runtime. Uno per la macchina target della lingua (COBOL per esempio). L'altra è la libreria di runtime dei compilatori di compilatori.

Penso di aver stabilito che questi non erano generatori di parser. Quindi ora con una piccola comprensione del back-end posso spiegare il linguaggio di programmazione del parser.

Linguaggio di programmazione parser

Il parser è scritto usando la formula scritta sotto forma di semplici equazioni.

<name> <formula type operator> <expression> ;

L'elemento linguaggio al livello più basso è il personaggio. I token sono formati da un sottoinsieme dei caratteri della lingua. Le classi di caratteri vengono utilizzate per nominare e definire quei sottoinsiemi di caratteri. L'operatore che definisce la classe di caratteri è i due punti (:). I personaggi membri della classe sono codificati sul lato destro della definizione. I caratteri stampabili sono racchiusi tra parentesi singole "stringhe". I caratteri non stampabili e speciali possono essere rappresentati dal loro ordinale numerico. I membri della classe sono separati da un'alternativa | operatore. Una formula di classe termina con un punto e virgola. Le classi di caratteri possono includere classi precedentemente definite:

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

Skip_class 0b00000001 è predefinito ma potrebbe essere fuori strada la definizione di skip_class.

In breve: una classe di caratteri è un elenco di alternative che può essere solo una costante di carattere, un carattere ordinale o una classe di caratteri precedentemente definita. Durante l'implementazione delle classi di caratteri: alla formula di classe viene assegnata una maschera di bit di classe. (Indicato nei commenti sopra) Qualsiasi formula di classe con caratteri letterali o ordinali determina l'allocazione di un bit di classe. Una maschera viene creata ordinando le maschere di classe delle classi incluse insieme al bit allocato (se presente). Una tabella di classi viene creata dalle classi di caratteri. Una voce indicizzata dall'ordinale di un personaggio contiene bit che indicano le appartenenze alla classe del personaggio. Il test di classe viene eseguito in linea. Un esempio di codice IA-86 con l'ordinale del personaggio in eax illustra i test di classe:

test    byte ptr [eax+_classmap],dgt

Seguito da un:

jne      <success>

o

je       <failure>

Gli esempi di codice di istruzioni IA-86 sono usati perché penso che le istruzioni IA-86 siano oggi più conosciute. Il nome della classe che valuta la sua maschera di classe è AND non distruttivo con la tabella di classe indicizzata dai caratteri ordinali (in eax). Un risultato diverso da zero indica l'appartenenza alla classe. (EAX viene azzerato ad eccezione di al (gli 8 bit bassi di EAX) che contiene il carattere).

I token erano un po 'diversi in questi vecchi compilatori. Le parole chiave non sono state spiegate come token. Erano semplicemente abbinati alle costanti di stringa citate nel linguaggio parser. Le stringhe tra virgolette non vengono normalmente mantenute. I modificatori possono essere utilizzati. A + mantiene la stringa abbinata. (ie + '-' corrisponde a un carattere - mantenendo il carattere in caso di successo) L'operazione, (cioè, 'E') inserisce la stringa nel token. Lo spazio bianco viene gestito dalla formula token saltando i caratteri SKIP_CLASS principali fino a quando non viene effettuata una prima corrispondenza. Notare che una corrispondenza esplicita del carattere skip_class interromperà il salto consentendo a un token di iniziare con un carattere skip_class. La formula del token di stringa salta i caratteri skip_class iniziali che corrispondono a un carattere chiuso tra virgolette singole o una stringa tra virgolette doppie. Di interesse è la corrispondenza di un "carattere all'interno di" una stringa tra virgolette:

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

La prima alternativa corrisponde a qualsiasi carattere tra virgolette singole. L'alternativa giusta corrisponde a una stringa tra virgolette doppie che può includere caratteri di virgolette doppie usando due caratteri "insieme per rappresentare un singolo" carattere. Questa formula definisce le stringhe utilizzate nella propria definizione. L'alternativa destra interna '"' $ (-" "" ".ANY |" "" "" "," "" ") '"' corrisponde a una stringa tra virgolette doppie. Possiamo usare un singolo carattere tra virgolette per abbinare un carattere tra virgolette doppie. Tuttavia all'interno della doppia stringa tra virgolette se vogliamo usare un carattere "dobbiamo usare due" caratteri per ottenerne uno. Ad esempio, nell'alternativa interna sinistra che corrisponde a qualsiasi carattere tranne una citazione:

-"""" .ANY

una sbirciatina negativa in avanti - "" "" viene utilizzato che quando ha esito positivo (non corrisponde a un "carattere) corrisponde quindi al carattere .ANY (che non può essere un" carattere perché - "" "" ha eliminato quella possibilità). L'alternativa giusta sta assumendo: "" "" abbinare un carattere "e fallire erano l'alternativa giusta:

"""""",""""

prova a far corrispondere due "caratteri sostituendoli con un singolo doppio" usando, "" "" per inserire il singolo carattere ". Entrambe le alternative interne che falliscono il carattere di citazione della stringa di chiusura vengono abbinate e MAKSTR [] ha chiamato per creare un oggetto stringa. sequenza, loop con esito positivo, l'operatore viene utilizzato nella corrispondenza di una sequenza. La formula token salta i caratteri della classe skip principale (con spazio). Una volta effettuata una prima corrispondenza skip_class il salto è disabilitato. È possibile chiamare le funzioni programmate in altre lingue utilizzando []. MAKSTR [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] e MAKINT [] sono forniti con la funzione di libreria che converte una stringa di token corrispondente in un oggetto tipizzato. La formula numerica seguente mostra un riconoscimento token abbastanza complesso:

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

La formula del token numerico sopra riportata riconosce numeri interi e in virgola mobile. Le alternative - hanno sempre successo. Gli oggetti numerici possono essere utilizzati nei calcoli. Gli oggetti token vengono inseriti nello stack di analisi in caso di successo della formula. Il vantaggio dell'esponente in (+ 'E' | 'e', ​​'E') è interessante. Desideriamo avere sempre una E maiuscola per MAKEFLOAT []. Ma consentiamo una "e" minuscola che la sostituisce usando "E".

Potresti aver notato consistenze di classe di caratteri e formula di token. La formula di analisi prosegue aggiungendo alternative di backtracking e operatori di costruzione dell'albero. Gli operatori alternativi di backtracking e non di backtracking non possono essere mescolati all'interno di un livello di espressione. Potresti non avere (a | b \ c) che mescola senza backtrack | con alternativa \ backtracking. (a \ b \ c), (a | b | c) e ((a | b) \ c) sono validi. Un'alternativa \ backtracking salva lo stato di analisi prima di tentare la sua alternativa sinistra e in caso di errore ripristina lo stato di analisi prima di tentare la giusta alternativa. In una sequenza di alternative la prima alternativa di successo soddisfa il gruppo. Non vengono tentate ulteriori alternative. Il factoring e il raggruppamento prevedono un continuo avanzamento dell'analisi. L'alternativa backtrack crea uno stato salvato dell'analisi prima di tentare la sua alternativa sinistra. Il backtracking è necessario quando l'analisi può effettuare una corrispondenza parziale e quindi fallire:

(a b | c d)\ e

Quanto sopra in caso di errore di restituzione viene tentato il cd alternativo. Se poi c restituisce un errore, verrà tentata l'alternativa di backtrack. Se a ha esito positivo e b non riesce, l'analisi verrà retrocessa e verrà tentata. Allo stesso modo, un errore c ha esito positivo e b non riesce l'analisi viene ritracciata e l'alternativa e presa. Il backtracking non è limitato a una formula. Se una formula di analisi fa una corrispondenza parziale in qualsiasi momento e poi fallisce, l'analisi viene ripristinata al backtrack superiore e viene presa la sua alternativa. Un errore di compilazione può verificarsi se il codice è stato emesso nel senso che la backtrack è stata creata. Un backtrack viene impostato prima di iniziare la compilazione. Restituire un errore o tornare indietro è un errore del compilatore. I backtracks sono impilati. Possiamo usare il negativo - e il positivo? sbirciare / guardare avanti operatori per testare senza far avanzare l'analisi. essendo test delle stringhe è una sbirciatina in avanti che necessita solo dello stato di input salvato e ripristinato. Uno sguardo al futuro sarebbe un'espressione di analisi che crea una corrispondenza parziale prima di fallire. Uno sguardo al futuro è implementato usando il backtracking.

Il linguaggio parser non è né un parser LL né LR. Ma un linguaggio di programmazione per scrivere un parser decente ricorsivo in cui si programma la costruzione di alberi:

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

Un esempio di analisi comunemente usato è un'espressione aritmetica:

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

Exp e Term usando un loop crea un albero per mancini. Il fattore che utilizza la giusta ricorsione crea un albero per la mano destra:

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

Ecco un po 'del compilatore cc, una versione aggiornata di SLIC con commenti in stile c. I tipi di funzione (grammatica, token, classe di caratteri, generatore, PSEUDO o MACHOP sono determinati dalla sintassi iniziale che segue il loro ID. Con questi parser top-down si inizia con una formula che definisce il programma:

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

// Nota come ID viene scomposto e successivamente combinato durante la creazione dell'albero.

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

Da notare come il linguaggio del parser gestisce i commenti e il recupero degli errori.

Penso di aver risposto alla domanda. Dopo aver scritto gran parte del successore degli SLIC, il linguaggio cc in sé è qui. Non esiste ancora un compilatore. Ma posso compilarlo a mano in codice assembly, funzioni bare asm c o c ++.


0

Sì, puoi scrivere un compilatore per una lingua in quella lingua. No, non è necessario un primo compilatore per quella lingua da avviare.

Ciò di cui hai bisogno per avviare bootstrap è un'implementazione della lingua. Può essere un compilatore o un interprete.

Storicamente, le lingue erano generalmente pensate come lingue interpretate o lingue compilate. Gli interpreti sono stati scritti solo per il primo e i compilatori sono stati scritti solo per il secondo. Quindi di solito se un compilatore dovesse essere scritto per una lingua, il primo compilatore verrebbe scritto in un'altra lingua per avviarlo, quindi, facoltativamente, il compilatore verrebbe riscritto per la lingua dell'oggetto. Ma scrivere un interprete in un'altra lingua è invece un'opzione.

Questo non è solo teorico. Attualmente mi capita di farlo da solo. Sto lavorando a un compilatore per una lingua, Salmon, che ho sviluppato da solo. Prima ho creato un compilatore Salmon in C e ora sto scrivendo il compilatore in Salmon, quindi posso far funzionare il compilatore Salmon senza mai avere un compilatore per Salmon scritto in qualsiasi altra lingua.


-1

Forse puoi scrivere un BNF che descrive BNF.


4
Puoi davvero (non è poi così difficile), ma la sua unica applicazione pratica sarebbe in un generatore di parser.
Daniel Spiewak,

In effetti ho usato proprio questo metodo per produrre il generatore di parser LIME. Una rappresentazione tabulare, ristretta, semplificata della metagrammar passa attraverso un semplice parser a discesa ricorsiva. Quindi, LIME genera un parser per il linguaggio delle grammatiche e quindi utilizza quel parser per leggere la grammatica per cui qualcuno è effettivamente interessato a generare un parser. Questo significa che non devo sapere come scrivere quello che ho appena scritto. Sembra magia.
Ian,

In realtà non puoi, dato che BNF non può descrivere se stesso. È necessaria una variante come quella usata in yacc in cui i simboli non terminali non sono citati.
Marchese di Lorne,

1
Non è possibile utilizzare bnf per definire bnf poiché <> non può essere riconosciuto. EBNF ha risolto il problema citando i token di stringa costanti della lingua.
GK,
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.