Compilatore
In informatica, un compilatore è un programma traduttore, impiegato per produrre codice oggetto (in linguaggio macchina) a partire da codice sorgente scritto in un dato linguaggio di programmazione di livello più alto. Questo processo si chiama compilazione. Una volta prodotto il codice oggetto (ovvero un file eseguibile) risulta estremamente difficoltoso risalire al sorgente; questa procedura viene chiamata reversing.
I linguaggi interpretati non hanno bisogno di essere compilati perché sono eseguiti mediante un interprete.
Il compilatore è una parte fondamentale di tutti i linguaggi compilati. Per un dato linguaggio possono esistere uno o più compilatori, sviluppati da persone o ditte diverse, gratuiti o a pagamento, open source oppure no. Se tutti i compilatori aderiscono esattamente alla specifica del linguaggio, lo stesso programma potrà essere compilato senza modifiche da ciascun compilatore, producendo risultati sematicamente uguali, ovvero programmi che producono lo stesso risultato se sottoposti agli stessi input. Nella realtà, molti compilatori implementano il linguaggio in modo incompleto, o aggiungono estensioni proprietarie, creando in effetti dei dialetti di ciascun linguaggio. Per i linguaggi che adottano uno standard nella decorazione dei simboli, il codice oggetto generato da compilatori differenti può essere linkato assieme in un unico eseguibile.
Un compilatore, in genere, è in grado di tradurre un solo linguaggio di alto livello in codice oggetto, anche se alcuni progetti producono compilatori più linguaggi, come fa ad esempio gcc.
| Indice |
Schema di funzionamento
Il compilatore prende in input un programma su cui esegue una serie di operazioni in modo da ottenere, se possibile, un programma eseguibile.
In generale i compilatori sono in grado di riconoscere alcune classi di errori presenti nel programma, e in alcuni casi di suggerire in che modo correggerli.
Analisi lessicale
Per prima cosa il programma viene suddiviso in stringhe note, come ad esempio parole chiave del linguaggio (while, for), costanti e nomi di variabili, i cosiddetti token.
In questa fase possono anche essere riconosciute sequenze non valide, come variabili con nomi non leciti, caratteri che non fanno parte del linguaggio ecc. In più l'analizzatore lessicale può generare una lista di nomi di variabili usate nel programma.
L'analizzatore lessicale è, di solito, costruito con un programma ad hoc che genera un parser a partire da espressioni regolari.
Analisi sintattica e semantica
In questa fase il compilatore crea un albero sintattico partendo dai token. L'albero è costruito in base a una grammatica che specifica le possibili sequenze di token accettate del linguaggio.
L'analisi sintattica si occupa anche di rilevare errori nel programma, come sequenze errate di parole chiave, mancanza di determinati token, o errori nelle espressioni. Un buon compilatore non solo segnala gli errori, ma suggerisce le correzioni da apportare, almeno per gli errori più comuni.
Il controllo del tipo delle espressioni fa parte dell'analisi semantica: il compilatore verifica che a ogni variabile venga assegnato il tipo corretto, ed eventualmente modifica il tipo di un nodo dell'albero in modo che sia del tipo corretto (casting).
Controllando il tipo delle variabili e delle funzioni viene completata la tabella dei simboli, usata nel passo successivo.
Generazione del codice
Dall'albero sintattico e dalla tabella dei simboli vengono prese le informazioni per generare il codice eseguibile. A volte invece di generare immediatamente il codice oggetto, il compilatore crea un codice intermedio, di solito un codice a tre indirizzi.
Questo codice intermedio può ad esempio essere utilizzato su una macchina virtuale come ad esempio la Java Virtual Machine.
Il compilatore si occupa anche di creare tutte le strutture necessarie al funzionamento del programma, come i record di attivazione, usati per le chiamate a funzione.
Limitazioni
In teoria, un compilatore dovrebbe essere in grado di poter tradurre in codice oggetto tutti i costrutti possibili del linguaggio ad alto livello che sta compilando. Alcuni linguaggi sono tuttavia così complessi (come il C++) che anche i compilatori migliori tralasciano alcune delle parti più oscure e meno usate. Ovviamente, compilatori diversi tralasciano parti diverse del linguaggio, ed occorre fare attenzione nello scrivere il codice se si vuole che il proprio programma possa essere compilato da chiunque.
Ottimizzazione
Dato che un certo programma sorgente può essere convertito in una gamma quasi infinita di codici oggetto equivalenti tra loro dal punto di vista funzionale, il compito del compilatore non è solo quello di generare un codice oggetto funzionante: ormai tutti i compilatori moderni sono compilatori ottimizzanti, che cercano il miglior codice oggetto possibile, in relazione a parametri come lo spazio di memoria occupato o la velocità di esecuzione. In genere tali compilatori permettono un certo grado di controllo da parte dell'utente, perlomeno secondo quale direzione ottimizzare maggiormente.
Data la grande complessità del problema dell'ottimizzazione, il codice oggetto scritto a mano da un programmatore (in linguaggio assembly) è migliore di quello generato da un compilatore ottimizzante. Ma un programmatore può realisticamente scrivere in assembly solo programmi piccoli, o piccole porzioni di un programma più grande. La differenza inoltre è abbastanza piccola per cui, nel 99% dei casi, il codice generato dal compilatore è sufficiente.
Tuttavia, alcune parti di codice eseguite molto frequentemente, come ad esempio i gestori degli interrupt nei sistemi operativi, sono ancora scritte in assembly, perché in questi casi lo sforzo è ripagato dal guadagno in prestazioni.
