Come scrivere un compilatore molto semplice


214

Compilatori avanzati come gcccompilare codici in file leggibili dalla macchina in base alla lingua in cui è stato scritto il codice (ad es. C, C ++, ecc.). In effetti, interpretano il significato di ciascun codice in base alla libreria e alle funzioni delle lingue corrispondenti. Correggimi se sbaglio.

Vorrei capire meglio i compilatori scrivendo un compilatore molto semplice (probabilmente in C) per compilare un file statico (ad esempio Hello World in un file di testo). Ho provato alcuni tutorial e libri, ma tutti sono per casi pratici. Si occupano di compilare codici dinamici con significati connessi con la lingua corrispondente.

Come posso scrivere un compilatore di base per convertire un testo statico in un file leggibile dalla macchina?

Il prossimo passo sarà introdurre le variabili nel compilatore; immagina di voler scrivere un compilatore che compili solo alcune funzioni di una lingua.

L'introduzione di esercitazioni pratiche e risorse è molto apprezzata :-)



Hai provato lex / flex e yacc / bison?
mouviciel,

15
@mouviciel: non è un buon modo per imparare a costruire un compilatore. Questi strumenti fanno una notevole quantità di duro lavoro per te, quindi non lo fai mai realmente e impari come è fatto.
Mason Wheeler,

11
@Mat in modo interessante, il primo dei tuoi link dà 404, mentre il secondo è ora contrassegnato come duplicato di questa domanda.
Ruslan,

Risposte:


326

Intro

Un compilatore tipico esegue le seguenti operazioni:

  • Analisi: il testo di origine viene convertito in un albero di sintassi astratto (AST).
  • Risoluzione di riferimenti ad altri moduli (C rimanda questo passaggio fino al collegamento).
  • Convalida semantica: eliminando le affermazioni sintatticamente corrette che non hanno senso, ad esempio codice irraggiungibile o dichiarazioni duplicate.
  • Trasformazioni equivalenti e ottimizzazione di alto livello: l'AST viene trasformato per rappresentare un calcolo più efficiente con la stessa semantica. Ciò include ad esempio il calcolo precoce delle sottoespressioni comuni e delle espressioni costanti, l'eliminazione di assegnazioni locali eccessive (vedere anche SSA ), ecc.
  • Generazione di codice: l'AST si trasforma in codice lineare di basso livello, con salti, allocazione dei registri e simili. Alcune chiamate di funzione possono essere integrate in questa fase, alcuni loop srotolati, ecc.
  • Ottimizzazione dello spioncino: il codice di basso livello viene scansionato alla ricerca di semplici inefficienze locali che vengono eliminate.

La maggior parte dei compilatori moderni (ad esempio gcc e clang) ripetono ancora una volta gli ultimi due passaggi. Usano un linguaggio intermedio di basso livello ma indipendente dalla piattaforma per la generazione iniziale del codice. Quindi quel linguaggio viene convertito in codice specifico della piattaforma (x86, ARM, ecc.) Facendo all'incirca la stessa cosa in modo ottimizzato per la piattaforma. Ciò include, ad esempio, l'uso di istruzioni vettoriali quando possibile, il riordino delle istruzioni per aumentare l'efficienza di previsione del ramo e così via.

Successivamente, il codice oggetto è pronto per il collegamento. La maggior parte dei compilatori di codice nativo sa come chiamare un linker per produrre un eseguibile, ma non è di per sé un passaggio di compilazione. In linguaggi come Java e C # il collegamento può essere totalmente dinamico, fatto dalla VM al momento del caricamento.

Ricorda le basi

  • Fallo funzionare
  • Rendilo bellissimo
  • Rendilo efficiente

Questa sequenza classica si applica a tutto lo sviluppo del software, ma porta ripetizione.

Concentrati sul primo passo della sequenza. Crea la cosa più semplice che potrebbe funzionare.

Leggi i libri!

Leggi il Dragon Book di Aho e Ullman. Questo è classico ed è ancora abbastanza applicabile oggi.

Anche il design moderno del compilatore è elogiato.

Se questa roba è troppo difficile per te in questo momento, leggi prima alcune introduzioni sull'analisi; di solito l'analisi delle librerie include introduzioni ed esempi.

Assicurati di lavorare comodamente con i grafici, in particolare gli alberi. Queste cose sono le cose di cui sono fatti i programmi a livello logico.

Definisci bene la tua lingua

Usa qualunque notazione desideri, ma assicurati di avere una descrizione completa e coerente della tua lingua. Ciò include sia la sintassi che la semantica.

È giunto il momento di scrivere frammenti di codice nella tua nuova lingua come casi di test per il futuro compilatore.

Usa la tua lingua preferita

È assolutamente OK scrivere un compilatore in Python o Ruby o in qualsiasi lingua sia facile per te. Usa semplici algoritmi che capisci bene. La prima versione non deve essere veloce, efficiente o completa di funzionalità. Deve solo essere abbastanza corretto e facile da modificare.

Va anche bene scrivere diverse fasi di un compilatore in diverse lingue, se necessario.

Preparati a scrivere molti test

L'intera lingua dovrebbe essere coperta da casi di test; efficacemente sarà definito da loro. Conosci bene il tuo framework di test preferito. Scrivi i test dal primo giorno. Concentrarsi su test "positivi" che accettano il codice corretto, anziché il rilevamento di un codice errato.

Esegui tutti i test regolarmente. Correggere i test rotti prima di procedere. Sarebbe un peccato finire con un linguaggio mal definito che non può accettare un codice valido.

Crea un buon parser

I generatori di parser sono molti . Scegli quello che vuoi. Si può anche scrivere il proprio parser da zero, ma solo vale la pena se la sintassi del linguaggio è morto semplice.

Il parser dovrebbe rilevare e segnalare errori di sintassi. Scrivi molti casi di test, sia positivi che negativi; riutilizzare il codice che hai scritto durante la definizione della lingua.

L'output del tuo parser è un albero di sintassi astratto.

Se la tua lingua ha dei moduli, l'output del parser potrebbe essere la rappresentazione più semplice del "codice oggetto" che generi. Esistono molti modi semplici per scaricare un albero in un file e ricaricarlo rapidamente.

Crea un validatore semantico

Molto probabilmente il tuo linguaggio consente costruzioni sintatticamente corrette che potrebbero non avere senso in determinati contesti. Un esempio è una dichiarazione duplicata della stessa variabile o il passaggio di un parametro di tipo errato. Il validatore rileverà tali errori guardando l'albero.

Il validatore risolverà anche i riferimenti ad altri moduli scritti nella tua lingua, caricherà questi altri moduli e li userà nel processo di validazione. Ad esempio, questo passaggio assicurerà che il numero di parametri passati a una funzione da un altro modulo sia corretto.

Ancora una volta, scrivi ed esegui molti casi di test. I casi fondamentali sono indispensabili per la risoluzione dei problemi quanto intelligenti e complessi.

Genera codice

Usa le tecniche più semplici che conosci. Spesso è OK tradurre direttamente un costrutto linguistico (come ifun'istruzione) in un modello di codice leggermente parametrizzato, non diversamente da un modello HTML.

Ancora una volta, ignora l'efficienza e concentrati sulla correttezza.

Targeting di una macchina virtuale di basso livello indipendente dalla piattaforma

Suppongo che tu ignori cose di basso livello a meno che non sia profondamente interessato ai dettagli specifici dell'hardware. Questi dettagli sono cruenti e complessi.

Le tue opzioni:

  • LLVM: consente una generazione efficiente del codice macchina, in genere per x86 e ARM.
  • CLR: target .NET, principalmente x86 / basato su Windows; ha una buona squadra.
  • JVM: si rivolge al mondo Java, piuttosto multipiattaforma, ha una buona JIT.

Ignora l'ottimizzazione

L'ottimizzazione è difficile. Quasi sempre l'ottimizzazione è prematura. Genera codice inefficiente ma corretto. Implementa l'intera lingua prima di provare a ottimizzare il codice risultante.

Ovviamente, sono ottimali banali ottimizzazioni da introdurre. Ma evita tutte le cose astute e pelose prima che il tuo compilatore sia stabile.

E allora?

Se tutte queste cose non ti intimidiscono troppo, procedi! Per un linguaggio semplice, ciascuno dei passaggi può essere più semplice di quanto si pensi.

Vedere un "Hello world" da un programma creato dal tuo compilatore potrebbe valere la pena.


45
Questa è una delle migliori risposte che abbia mai visto.
gahooa,

11
Penso che ti sia sfuggita una parte della domanda ... L'OP voleva scrivere un compilatore molto semplice . Penso che tu vada oltre molto semplice qui.
marco-fiset,

22
@ marco-fiset , al contrario, penso che sia una risposta eccezionale che dice all'OP come fare un compilatore molto semplice, sottolineando le trappole per evitare e definire fasi più avanzate.
smci

6
Questa è una delle migliori risposte che io abbia mai visto nell'intero universo di Stack Exchange. Complimenti!
Andre Terra,

3
Vedere un "Hello world" da un programma creato dal tuo compilatore potrebbe valere la pena. -
INDEED

27

Let's Build a Compiler di Jack Crenshaw , sebbene incompiuto, è un'introduzione e un tutorial estremamente leggibili.

La costruzione del compilatore di Nicklaus Wirth è un ottimo libro di testo sulle basi della semplice costruzione di un compilatore. Si concentra sulla discesa ricorsiva dall'alto verso il basso, che, ammettiamolo, è MOLTO più facile di lex / yacc o flex / bison. Il compilatore originale PASCAL che il suo gruppo ha scritto è stato fatto in questo modo.

Altre persone hanno menzionato i vari libri di Dragon.


1
Una delle cose belle di Pascal è che tutto deve essere definito o dichiarato prima di essere utilizzato. Pertanto può essere compilato in un unico passaggio. Turbo Pascal 3.0 è un esempio, e non v'è molta documentazione riguardo la struttura interna qui .
Tcrosley,

1
PASCAL è stato progettato specificatamente pensando alla compilazione e al collegamento in un passaggio. Il libro del compilatore di Wirth menziona i compilatori multipass e aggiunge che sapeva di un compilatore PL / I che impiegava 70 (sì, settanta) passaggi.
John R. Strohm,

La dichiarazione obbligatoria prima dell'uso risale ad ALGOL. Tony Hoare è stato bloccato dal comitato ALGOL quando ha cercato di suggerire di aggiungere regole di tipo predefinite, simili a quelle di FORTRAN. Conoscevano già i problemi che questo poteva creare, con errori tipografici nei nomi e regole predefinite che creavano bug interessanti.
John R. Strohm,

1
Ecco una versione più aggiornata e completa del libro dell'autore originale stesso: stack.nl/~marcov/compiler.pdf Modifica la tua risposta e aggiungila :)
sonetto

16

In realtà inizierei con la scrittura di un compilatore per Brainfuck . È un linguaggio abbastanza ottuso da programmare ma ha solo 8 istruzioni da implementare. È il più semplice possibile e ci sono equivalenti istruzioni C per i comandi coinvolti se trovi la sintassi scoraggiante.


7
Ma poi, una volta che hai il compilatore BF pronto, devi scrivere il tuo codice in esso :(
500 - Errore interno del server

@ 500-InternalServerError usa il metodo del sottoinsieme C
World Engineer

12

Se vuoi davvero scrivere solo codice leggibile dalla macchina e non indirizzato a una macchina virtuale, dovrai leggere i manuali Intel e capire

  • un. Collegamento e caricamento del codice eseguibile

  • b. Formati COFF e PE (per Windows), in alternativa capire il formato ELF (per Linux)

  • c. Comprendi i formati di file .COM (più facili di PE)
  • d. Comprendere gli assemblatori
  • e. Comprendi i compilatori e il motore di generazione del codice nei compilatori.

Molto più difficile di quanto detto. Ti suggerisco di leggere compilatori e interpreti in C ++ come punto di partenza (di Ronald Mak). In alternativa, "consente di compilare un compilatore" di Crenshaw è OK.

Se non vuoi farlo, puoi anche scrivere la tua VM e scrivere un generatore di codice destinato a quella VM.

Suggerimenti: impara prima Flex e Bison. Quindi continua a creare il tuo compilatore / VM.

In bocca al lupo!


7
Penso che il targeting per LLVM e non il vero codice macchina sia il modo migliore disponibile oggi.
9000

Sono d'accordo, seguo LLVM da un po 'di tempo e dovrei dire che è stata una delle cose migliori che ho visto da anni in termini di sforzo del programmatore necessario per indirizzarlo!
Aniket Inge,

2
Che dire di MIPS e utilizzare spim per eseguirlo? O MIX ?

@MichaelT Non ho usato MIPS ma sono sicuro che andrà bene.
Aniket Inge,

Set di istruzioni RISC @PrototypeStark, processore del mondo reale che è ancora in uso oggi (capendo che sarà traducibile in sistemi integrati). Il set di istruzioni completo è su Wikipedia . Guardando in rete, ci sono molti esempi ed è usato in molte classi accademiche come obiettivo per la programmazione del linguaggio automatico. C'è un po 'di attività su SO .

10

L'approccio fai-da-te per un semplice compilatore potrebbe apparire così (almeno così sembrava il mio progetto uni):

  1. Definisci la grammatica della lingua. -Context gratuito.
  2. Se la tua grammatica non è ancora LL (1), fallo ora. Nota che alcune regole che sembravano ok nella semplice grammatica CF potrebbero rivelarsi brutte. Forse la tua lingua è troppo complessa ...
  3. Scrivi Lexer che taglia il flusso di testo in token (parole, numeri, letterali).
  4. Scrivi un parser discendente ricorsivo top-down per la tua grammatica, che accetta o rifiuta l'input.
  5. Aggiungi la generazione dell'albero di sintassi nel tuo parser.
  6. Scrivi il generatore di codice macchina dall'albero della sintassi.
  7. Profit & Beer, in alternativa puoi iniziare a pensare a come eseguire un parser più intelligente o generare codice migliore.

Ci dovrebbe essere molta letteratura che descriva dettagliatamente ogni passaggio.


Il settimo punto è ciò di cui l'OP chiede.
Florian Margaine,

7
1-5 sono irrilevanti e non meritano una tale attenzione. 6 è la parte più interessante. Sfortunatamente, la maggior parte dei libri segue lo stesso schema, dopo il famigerato libro dei draghi, prestando troppa attenzione all'analisi e lasciando che il codice si trasformi fuori campo.
SK-logic,
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.