Ciao! Se è la prima volta che capiti su questo sito, ti consiglio di consultare la pagina principale di questo Giardino Digitale per scoprire meglio cos’è e come navigarlo.
Lo stato di questa nota è al momento: 🔴 Bozza.
All’alba dei tempi della programmazione (ossia intorno agli anni ‘40/’50), i programmi venivano scritti direttamente in linguaggio macchina poiché non esistevano ancora i linguaggi di programmazione: la programmazione era ovviamente molto difficile perché per noi umani non è semplice dover tradurre le istruzioni che vogliamo far eseguire a un computer in una sequenza di zeri e uno, tenendo anche conto del fatto che ogni computer ha una propria architettura e, di conseguenza, un programma scritto per una determinata architettura non funzionerà su ogni computer.
Per questo motivo sono stati inventati i linguaggi di programmazione, ossia linguaggi che ci permettono di scrivere programmi in un formato più leggibile a noi umani (usando parole inglesi al posto di 0 e 1).
Tuttavia c’è un problema: un computer è in grado di comprendere solo il proprio linguaggio macchina e non capisce i linguaggi di programmazione. Ecco che allora serve un “traduttore” che prenda i programmi scritti in un determinato linguaggio di programmazione e li trasformi in un altro linguaggio (per esempio il linguaggio macchina): questi “traduttori” sono detti compilatori.
Osservazione: a cosa serve studiare i compilatori?
Al giorno d’oggi (quasi) nessuno realizza più compilatori (eccetto se si sta inventando un nuovo linguaggio di programmazione), ma allora a cosa serve studiarli?
Innanzitutto, come per qualsiasi altra cosa nel mondo dell’informatica (e non solo), concetti e metodi studiati qua possono essere applicati anche in altri ambiti, come la lettura e l’analisi di dati organizzati in strutture dati, lo studio di linguaggi formali, ecc.
1 - Dal sorgente all’eseguibile
Oltre ai compilatori, molti altri programmi possono essere necessari per ottenere un eseguibile:
Un programma sorgente può essere diviso in moduli conservati in file separati e chi riordina questi file è il preprocessore, che per esempio può anche risolvere macro
Il codice sorgente modificato dal preprocessore viene dato in pasto al compilatore che produce l’assembly
L’assembly viene processato dall’assembler che produce relocatable machine code
Il linker risolve gli indirizzi di memoria esterna che nel codice fa riferimento a librerie esterne
Il loader monta insieme i pezzi di relocatable machine code e librerie in un unico eseguibile
2 - Fasi della compilazione
Un compilatore non è un unico, ma diviso in due parti principali: analisi e sintesi.
Nella parte di analisi spezzetta il codice sorgente in più pezzi su cui fa controlli della correttezza della sua scrittura (e nel caso segnalare eventuali errori così che l’utente possa risolverli) e crea una symbol table in cui conserva i dati iniziali.
Nella parte di sintesi costruisce il programma destinazione dai risultati della parte di analisi.
Alcuni compilatori distinguono l’ottimizzazione tra machine-independent (cioè ottimizzazione che riguarda unicamente il codice in sè) e machine-dependent (cioè ottimizzazione specifica per la macchina su cui sta venendo eseguito).
2.1 - Analisi lessicale
Definizione: analisi lessicale
L’analisi lessicale è una delle fasi della compilazione in cui il codice viene convertito in blocchi chiamati token. Il programma che effettua questa operazione è detto analizzatore lessicale (in inglese lexer o scanner).
Definizione: token
Un token è una struttura dati prodotta dall’analisi lessicale composta da una categoria lessicale, che identifica il tipo di elemento riconosciuto, e da un eventuale lessema, ovvero la sequenza specifica di caratteri del codice sorgente che corrisponde al token, sotto forma di riferimento alla relativa entry nella tabella dei simboli.
Osservazione: facoltatività dei lessemi
Nella definizione di token c’è scritto “eventuale lessema” perché, a seconda dell’implementazione, un token può non conservare esplicitamente la sequenza originale di caratteri: per esempio, un token che nel linguaggio C ha come categoria “operatore di assegnazione” non può avere un lessema diverso da =, motivo per cui quest’ultimo può essere omesso.
Definizione: tabella dei simboli
La tabella dei simboli (in inglese symbol table) è una struttura dati utilizzata dal compilatore per memorizzare i lessemi dell’analisi lessicale e associarli ai corrispettivi token.
Prendiamo per esempio un codice sorgente in C che contiene la seguente assegnazione:
position = initial + rate * 60
L’analizzatore lessicale spezzetterà questo codice nei lessemiposition, =, initial, +, rate, * e 60 e farà le seguenti valutazioni, tenendo in mente che i nomi usati per le categorie sono puramente arbitrari e potrebbero non corrispondere a reali implementazioni di analizzatori:
position è un lessema che verrà associato al token⟨id,0⟩ dove id indica la categoria degli identificatori e 0 è la posizione a cui è memorizzata l’entry position nella tabella dei simboli.
= è un lessema che verrà associato al token⟨=⟩ ma, dal momento che il token non può avere altri valori al di fuori di =, non è necessario associarci un’entry nella tabella dei simboli.
initial è un lessema che verrà associato al token⟨id,1⟩ dove 1 è la posizione a cui è memorizzata l’entry initial nella tabella dei simboli.
60 è un lessema che verrà associato al token⟨num,3⟩ dove num indica la categoria dei numeri e 2 è la posizione a cui è memorizzata l’entry 60 nella tabella dei simboli.
Un interprete è un programma che, dato un codice, lo esegue direttamente senza doverlo compilare.
Il codice macchina prodotto da un compilatore è spesso molto più veloce da eseguire rispetto a un interprete che lo esegue direttamente, ma quest’ultimo può essere in grado di rilevare errori molto meglio di un compilatore perché esegue le istruzioni del codice sorgente una per volta.
Esempio: compilazione e interpretazione di Java
Gli elaboratori per il linguaggio Java combinano compilatori e interpreti: un codice sorgente scritto in Java viene prima compilato in un formato intermedio chiamato bytecode e poi quest’ultimo viene interpretato da una macchina virtuale.