Compilatore - Generazione di codice intermedio

Un codice sorgente può essere tradotto direttamente nel suo codice macchina di destinazione, allora perché dobbiamo tradurre il codice sorgente in un codice intermedio che viene poi tradotto nel suo codice di destinazione? Vediamo i motivi per cui abbiamo bisogno di un codice intermedio.

  • Se un compilatore traduce la lingua di origine nel linguaggio macchina di destinazione senza avere l'opzione per la generazione di codice intermedio, per ogni nuova macchina è richiesto un compilatore nativo completo.

  • Il codice intermedio elimina la necessità di un nuovo compilatore completo per ogni macchina unica, mantenendo la stessa porzione di analisi per tutti i compilatori.

  • La seconda parte del compilatore, la sintesi, viene modificata in base alla macchina di destinazione.

  • Diventa più facile applicare le modifiche al codice sorgente per migliorare le prestazioni del codice applicando tecniche di ottimizzazione del codice sul codice intermedio.

Rappresentazione intermedia

I codici intermedi possono essere rappresentati in vari modi e hanno i loro vantaggi.

  • High Level IR- La rappresentazione del codice intermedio di alto livello è molto simile alla lingua di origine stessa. Possono essere facilmente generati dal codice sorgente e possiamo facilmente applicare modifiche al codice per migliorare le prestazioni. Ma per l'ottimizzazione della macchina target, è meno preferito.

  • Low Level IR - Questo è vicino alla macchina di destinazione, il che lo rende adatto per l'allocazione di registri e memoria, selezione di set di istruzioni, ecc.

Il codice intermedio può essere specifico della lingua (ad esempio, Byte Code per Java) o indipendente dalla lingua (codice a tre indirizzi).

Codice a tre indirizzi

Il generatore di codice intermedio riceve l'input dalla sua fase predecessore, l'analizzatore semantico, sotto forma di un albero di sintassi annotato. Quell'albero di sintassi può quindi essere convertito in una rappresentazione lineare, ad esempio, notazione postfissa. Il codice intermedio tende ad essere un codice indipendente dalla macchina. Pertanto, il generatore di codice presuppone di avere un numero illimitato di memoria (registro) per generare il codice.

Per esempio:

a = b + c * d;

Il generatore di codice intermedio proverà a dividere questa espressione in sottoespressioni e quindi a generare il codice corrispondente.

r1 = c * d;
r2 = b + r1;
a = r2

r utilizzato come registro nel programma di destinazione.

Un codice a tre indirizzi ha al massimo tre posizioni di indirizzi per calcolare l'espressione. Un codice a tre indirizzi può essere rappresentato in due forme: quadrupli e tripli.

Quadrupli

Ogni istruzione nella presentazione quadrupla è divisa in quattro campi: operatore, arg1, arg2 e risultato. L'esempio sopra è rappresentato di seguito in formato quadruplo:

Operazione arg 1 arg 2 risultato
* c d r1
+ b r1 r2
+ r2 r1 r3
= r3 un

Tripli

Ogni istruzione nella presentazione in triplo ha tre campi: op, arg1 e arg2. I risultati delle rispettive sottoespressioni sono indicati dalla posizione dell'espressione. Le triple rappresentano la somiglianza con il DAG e l'albero della sintassi. Sono equivalenti a DAG mentre rappresentano espressioni.

Operazione arg 1 arg 2
* c d
+ b (0)
+ (1) (0)
= (2)

Le triple affrontano il problema dell'immobilità del codice durante l'ottimizzazione, poiché i risultati sono posizionali e la modifica dell'ordine o della posizione di un'espressione può causare problemi.

Tripli indiretti

Questa rappresentazione è un miglioramento rispetto alla tripla rappresentazione. Utilizza i puntatori invece della posizione per memorizzare i risultati. Ciò consente agli ottimizzatori di riposizionare liberamente la sottoespressione per produrre un codice ottimizzato.

Dichiarazioni

Una variabile o una procedura deve essere dichiarata prima di poter essere utilizzata. La dichiarazione implica l'allocazione di spazio in memoria e l'immissione di tipo e nome nella tabella dei simboli. Un programma può essere codificato e progettato tenendo presente la struttura della macchina di destinazione, ma potrebbe non essere sempre possibile convertire accuratamente un codice sorgente nella sua lingua di destinazione.

Prendendo l'intero programma come una raccolta di procedure e sottoprocedure, diventa possibile dichiarare tutti i nomi locali alla procedura. L'allocazione della memoria viene eseguita in modo consecutivo ei nomi vengono assegnati alla memoria nella sequenza in cui sono dichiarati nel programma. Usiamo la variabile offset e la impostiamo a zero {offset = 0} che denota l'indirizzo di base.

Il linguaggio di programmazione sorgente e l'architettura della macchina di destinazione possono variare nel modo in cui i nomi vengono memorizzati, quindi viene utilizzato l'indirizzamento relativo. Mentre il primo nome è memoria allocata a partire dalla posizione di memoria 0 {offset = 0}, il nome successivo dichiarato in seguito, dovrebbe essere allocato memoria accanto al primo.

Example:

Prendiamo l'esempio del linguaggio di programmazione C in cui a una variabile intera vengono assegnati 2 byte di memoria e a una variabile float vengono assegnati 4 byte di memoria.

int a;
float b;

Allocation process:
{offset = 0}

   int a;
   id.type = int
   id.width = 2

offset = offset + id.width 
{offset = 2}

   float b;
   id.type = float
   id.width = 4
   
offset = offset + id.width 
{offset = 6}

Per inserire questo dettaglio in una tabella dei simboli, è possibile utilizzare una procedura di immissione . Questo metodo può avere la seguente struttura:

enter(name, type, offset)

Questa procedura dovrebbe creare una voce nella tabella dei simboli, per il nome della variabile , con il suo tipo impostato su tipo e relativo offset dell'indirizzo nella sua area dati.