1 / 55

Generazione di Codice Intermedio

Generazione di Codice Intermedio. Implementazione di Linguaggi 2 A.A. 2004/2005 di Gualdani Alessandro. Il generatore di codice intermedio nel compilatore. Interprete. Scanner. Parser. Type checker. Generatore di codice intermedio. Generatore di codice intermedio. Generatore di codice.

bowie
Download Presentation

Generazione di Codice Intermedio

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Generazione di Codice Intermedio Implementazione di Linguaggi 2 A.A. 2004/2005 di Gualdani Alessandro

  2. Il generatore di codice intermedio nel compilatore Interprete Scanner Parser Type checker Generatore di codice intermedio Generatore di codice intermedio Generatore di codice

  3. Introduzione Sebbene un programma sorgente possa essere tradotto direttamente in codice eseguibile, vi sono alcuni vantaggi nell’utilizzo di un codice intermedio indipendente dalla macchina: • Retargeting  un compilatore per macchine diverse può essere facilmente creato • Ottimizzazioni possono essere applicate alla rappresentazione intermedia

  4. Tipi di rappresentazioni intermedie • AST (Abstract Syntax Tree)  albero in cui un nodo rappresenta un operatore e i suoi figli rappresentano gli operandi • DAG (Directed Acyclic Graph)  simile ad un AST tranne per il fatto che una sottoespressione comune ha più di un padre • Notazione postfissa  è una linearizzazione di un AST: è una lista di nodi dell’albero in cui un nodo appare immediatamente dopo i suoi figli • Three address code (notazione a tre indirizzi)  descritto in seguito

  5. Esempio (1/2) Consideriamo il seguente esempio: a := b*-c + b*-c • DAG • AST := a + * b - c • := • a + • * * • b - b - • c c

  6. Esempio (2/2) • Notazione postfissa a b c - * b c - * + := • Three address code • Corrispondente all’AST • Corrispondente al DAG t1 := - c t2 := b * t1 t3 := - c t4 := b * t3 t5 := t2 + t4 a := t5 t1 := - c t2 := b * t1 t5 := t2 + t2 a := t5

  7. Three address code Il codice a tre indirizzi è una sequenza di istruzioni del tipo: x := y op z dove x, y, z sono nomi, costanti o temporanei generati dal compilatore ed op è un operatore Il nome “codice a tre indirizzi” deriva dal fatto che, di solito, ogni istruzione contiene tre indirizzi: due per gli operandi ed uno per il risultato

  8. Istruzioni del codice a tre indirizzi (1/3) Le istruzioni sono simili al codice assembler • possono avere etichette • ci sono istruzioni (salti) per il controllo di flusso • Comandi di assegnamento • della forma x := y op z, dove op è un operatore (binario) logico o aritmetico • della forma x := op y, dove op è un operatore unario • copy statement x := y, dove il valore di y è assegnato ad x

  9. Istruzioni del codice a tre indirizzi (2/3) • Salti • incondizionato, goto L • condizionato, if x relop y goto L, dove relop è un operatore relazionale (<, =, >=, …) • Invocazioni/ritorni di procedura • istruzioni paramx, call p, n, returny (opzionale); il loro uso tipico è nella sequenza generata per l’invocazione di procedura p(x1, x2, …, xn) paramx1 paramx2 … paramxn callp, n

  10. Istruzioni del codice a tre indirizzi (3/3) • Assegnamenti indiciati • della forma x := y[i], assegna alla variabile x il valore nella locazione che si trova i unità dopo y • della forma x[i] := y, l’opposto dell’assegnazione del punto precedente • Assegnamenti di indirizzi e puntatori • della forma x := &y, assegna ad x la locazione di y • della forma x := *y, assegna ad x il contenuto della locazione di y • della forma *x := y, assegna all’oggetto puntato da x il valore di y

  11. Implementazione dei comandi a tre indirizzi • In un compilatore i codici a tre indirizzi possono essere implementati come record con campi per gli operatori e gli operandi • Possibili rappresentazioni: • Quadruple • Triple • Triple indirette

  12. Implementazione dei comandi a tre indirizzi - Quadruple • Una quadrupla è un record con quattro campi op, arg1, arg2, result • op memorizza un codice interno per l’operatore • arg1 ed arg2 memorizzano il primo ed il secondo operando • result memorizza il risultato dell’istruzione • I contenuti di arg1, arg2, result sono, di solito, puntatori alla symbol table

  13. Implementazione dei comandi a tre indirizzi - Triple • Una tripla è un record con tre campi op, arg1, arg2 • op memorizza un codice interno per l’operatore • arg1 ed arg2 memorizzano il primo ed il secondo operando • Per evitare di inserire nomi temporanei nella symbol table, un valore intermedio viene riferito mediante la posizione dell’istruzione che lo calcola

  14. Implementazione dei comandi a tre indirizzi – Triple indirette • Le istruzioni del codice a tre indirizzi vengono rappresentate mediante una lista di puntatori a triple • Ad es., si può usare un array statement con l’elenco dei puntatori alle triple, nell’ordine desiderato

  15. Implementazione dei comandi a tre indirizzi – Esempio (1/3) • Consideriamo l’assegnamento a := b*-c + b*-c le rappresentazioni a quadruple, triple e triple indirette sono le seguenti: • Quadruple

  16. Implementazione dei comandi a tre indirizzi – Esempio (2/3) • Triple

  17. Implementazione dei comandi a tre indirizzi – Esempio (3/3) • Triple indirette

  18. Traduzione diretta dalla sintassi in three address code • Una regola (azione) semantica è assegnata ad ogni produzione • Si costruisce un AST e se ne effettua una visita depth-first per effettuare la traduzione in base alle regole semantiche • Nella fase di generazione del codice intermedio non è effettuata alcuna ottimizzazione

  19. Dichiarazioni Man mano che si incontrano le dichiarazioni in una procedura o in un blocco, si alloca lo spazio per la memoria: • per ogni nome locale, si crea una entrata nella symbol table con informazioni quali il tipo e l’indirizzo relativo per quel simbolo in memoria • nella generazione degli indirizzi si presuppone una particolare allocazione dei dati (nel seguito supporremo che gli indirizzi siano multipli di 4)

  20. Dichiarazioni in una procedura • Si utilizza una variabile globale offset per tener traccia del prossimo indirizzo di memoria libero • La procedura enter(name, type, offset) crea una nuova entrata nella symbol table per name, associandogli tipo type, a partire dall’indirizzo in offset • Il non terminale T possiede due attributi: • type (nome del tipo) • width (quantità di memoria necessaria per allocare valori del tipo T)

  21. Dichiarazioni – Schema di traduzione

  22. Comandi di assegnamento Attributi usati in seguito: • id.name: attributo che memorizza il nome dell’identificatore • E.place: attributo (di un’espressione E) che memorizza la variabile temporanea che contiene il valore dell’espressione

  23. Comandi di assegnamento Operazioni usate in seguito: • newtemp: funzione che restituisce l’indirizzo di una nuova variabile temporanea • lookup(id.name): funzione che verifica se esiste un’entrata per id.name nella symbol table; in caso positivo restituisce il puntatore (indirizzo di memoria) all’identificatore, altrimenti ritorna nil • emit(“code”): è una procedura che genera il codice a tre indirizzi

  24. Assegnamento – Schema di traduzione

  25. Espressioni booleane Nei linguaggi di programmazione le espressioni booleane hanno il duplice scopo: • di calcolare dei valori di verità • di controllare il flusso di esecuzione in statement quali, ad es., if o while In seguito considereremo espressioni booleane con la seguente grammatica: E  E or E | E and E | not E | ( E ) | idrelopid | true | false dove: • relop { <, >, =, , ,  } • gli operatori and e or associano a sinistra ed or ha precedenza minore di and e not

  26. Espressioni booleane – Metodi di rappresentazione I principali metodi per rappresentare i valori di un’espressione booleana sono: • codificare true e false attraverso valori interi (di solito 1 e 0) e valutare un’espressione booleana analogamente ad una aritmetica • rappresentare un’espressione booleana attraverso il flusso di controllo, cioè rappresentando il valore dell’espressione booleana mediante la posizione raggiunta nel programma

  27. Espressioni booleane – Rappresentazione numerica • Supponiamo true codificato con 1 e false con 0 • Ad es. a or b and not c è rappresentata dal seguente codice a tre indirizzi: • Una espressione relazionale come, ad es., a<b è equivalente all’istruzione condizionale if a<b then 1 else 0, che può essere tradotta nel seguente codice a tre indirizzi: t1 := not c t2 := b and t1 t3 := a or t2 100: if a<b goto 103 101: t := 0 102: goto 104 103: t := 1 104:

  28. Espressioni booleane – Rappresentazione numerica Operazioni usate in seguito: • nextstat: indirizzo della prossima istruzione • emit: incrementa nextstat dopo aver prodotto il codice a tre indirizzi

  29. Espressioni booleane – Rappresentazione numerica – Schema di traduzione

  30. Espressioni booleane – Rappresentazione attraverso il flusso di controllo • L’idea base è che la valutazione di una espressione produce due etichette: quella a cui si salta se l’espressione è vera e quella a cui si salta se l’espressione è falsa • Supponiamo, ad es., che l’espressione E sia della forma a<b; il codice a tre indirizzi è della forma: dove E.true è l’etichetta a cui si salta se l’espressione è vera ed E.false è l’etichetta a cui si salta se l’espressione è falsa if a<b goto E.true goto E.false

  31. Espressioni booleane – Rappresentazione attraverso il flusso di controllo • Supponiamo ora che E sia della forma E1or E2 • Se E1 è vera, allora sappiamo immediatamente che anche E è vera (così E1.true è lo stesso che E.true) • Se E1 è falsa, allora dobbiamo valutare E2 (così E1.false è l’etichetta della prima istruzione nel codice per E2; le uscite true e false di E2 sono le stesse di E) • Analoghe considerazioni si applicano alla traduzione E1and E2 • La traduzione di espressioni E della forma not E1 provoca solo uno scambio delle uscite true e false di E1 con quelle di E

  32. Espressioni booleane – Rappresentazione attraverso il flusso di controllo Attributi ed operazioni usati in seguito: • newlabel: funzione che ritorna una nuova etichetta • le espressioni booleane E hanno due attributi, E.true ed E.false, che contengono le etichette (indirizzi) dove bisogna saltare se l’espressione è vera o falsa ed un attributo E.code che contiene il codice prodotto per valutarle • l’operatore || rappresenta la concatenazione di codice

  33. Espressioni booleane – Rappresentazione attraverso il flusso di controllo – Schema di traduzione

  34. Comandi per il controllo del flusso La grammatica che consideriamo è la seguente: S  if E then S1 | if E then S1else S2 | while E do S1 • le espressioni booleane E vengono tradotte mediante rappresentazione attraverso il flusso di controllo • la traduzione degli statement S consente di controllare il flusso in base all’istruzione che deve seguire il codice S.code: viene realizzato attraverso l’attributo S.next che contiene l’etichetta della prima istruzione da eseguire dopo il codice per S

  35. to E.true E.code to E.false E.true: S1.code E.false: … Comandi per il controllo del flusso if E then S1 • Nella traduzione dello statement S if E then S1, una nuova etichetta E.true viene creata e “attaccata” alla prima istruzione generata per lo statement S1 • Il codice per E genera un salto all’etichetta E.true se E è vera ed un salto a S.next se E è falsa (quindi l’etichetta E.false è in corrispondenza della prossima istruzione da eseguire cioè S.next)

  36. to E.true E.code to E.false E.true: S1.code goto S.next E.false: S2.code S.next: … Comandi per il controllo del flusso if E then S1else S2 • Nella traduzione dello statement S if E then S1else S2 si salta alla prima istruzione di S1 se E è vera, alla prima istruzione di S2 se E è falsa • S.next, come nello statement S  if E thenS1, rappresenta l’etichetta della prima istruzione da eseguire dopo aver eseguito il codice per S

  37. to E.true S.begin: E.code to E.false E.true: S1.code goto S.begin E.false: … Comandi per il controllo del flusso while E do S1 • Nella traduzione dello statement S while E do S1, una nuova etichetta S.begin è creata ed “attaccata” alla prima istruzione generata per E ed un’altra E.true per la prima istruzione per S1 • Il codice per E genera un salto ad E.true se E è vera, ad S.next se E è falsa (cioè E.false “punta” ad S.next) • Dopo l’esecuzione del codice per S1 si effettua un salto ad S.begin per valutare l’espressione booleana

  38. Comandi per il controllo del flusso Schema di traduzione

  39. Invocazioni di procedure Consideriamo una semplice grammatica per invocare procedure: S  callid ( Elist ) Elist  Elist, E | E • Quando si genera il codice a tre indirizzi per le procedure, è necessario valutare le espressioni degli argomenti quindi fare seguire la lista di parametri; per far ciò: • si utilizza una coda (variabile globale queue) in cui i valori delle espressioni Elist vengono inseriti • la routine che implementa l’invocazione emette una istruzione param per ogni elemento della coda

  40. Invocazioni di procedure Schema di traduzione

  41. Backpatching (1/2) • Il modo più semplice per implementare le traduzioni finora presentate è quello di usare due passate: • prima si costruisce un albero sintattico per l’input • poi si effettua una visita depth-first dell’albero, calcolando le traduzioni date nella definizione

  42. Backpatching (2/2) • Il problema principale nel generare codice per le espressioni booleane e per il controllo di flusso in una singola passata è dato dal fatto che potremmo non conoscere l’etichetta argomento di una goto quando l’istruzione è generata • Se non si vogliono effettuare due passate, un modo per ovviare al problema è: • tener traccia di una lista di istruzioni goto con l’indirizzo del salto lasciato (temporaneamente) non specificato • riempire gli argomenti del salto quando l’indirizzo viene generato • Questa tecnica si chiama backpatching

  43. Argomenti non trattati • Dichiarazioni con procedure annidate [1, §8.2] • Dichiarazione di record [1, §8.2] • Indirizzamento di elementi in un array [1, §8.3] • Conversioni di tipo all’interno di assegnamenti [1, §8.3] • Istruzione case [1, §8.5]

  44. Un’altra rappresentazione intermedia • Michael Franz e Thomas Kistler hanno realizzato una rappresentazione intermedia che coniuga la portabilità del codice con le ridotte dimensioni in termini di spazio occupato • La rappresentazione intermedia da loro proposta va sotto il nome di slim binaries

  45. Slim Binaries

  46. Introduzione • I file oggetto contengono una rappresentazione intermedia compatta • La codifica degli slim binaries è basata sull’osservazione che parti differenti di un programma sono spesso simili le une alle altre; queste similarità vengono sfruttate utilizzando un algoritmo di compressione predittivo che consente di codificare le sottoespressioni ricorrenti in un programma in modo efficiente dal punto di vista dello spazio • La generazione di codice macchina per la specifica architettura viene effettuata on-the-fly

  47. Schema di compressione • Lo schema di compressione adattivo codifica l’input utilizzando un dizionario • Inizialmente il dizionario consiste di • un piccolo numero di operazioni primitive (quali assignment, addition e multiplication) • un piccolo numero di data item che appaiono nel programma che si sta processando (ad es. integer i, procedure P)

  48. Generazione della rappresentazione intermedia La traduzione da codice sorgente in rappresentazione intermedia si compone di due fasi: • si fa il parsing del codice sorgente e si costruisce un AST ed una symbol table; dopo la fase di parsing, la symbol table viene scritta sullo slim binary (servirà per inizializzare i data symbol del dizionario e per informazioni di tipo per il generatore di codice) • l’AST è attraversato e codificato in una sequenza di simboli del dizionario

  49. Esplicitazione della Fase 2 (1/2) • L’encoder processa interi sottoalberi dell’AST alla volta e per ognuno ricerca nel dizionario una sequenza di simboli che esprima lo stesso significato • Es. la chiamata di procedura P(i+1) può essere rappresentata dalla combinazione delle operazioni procedure call e addition e dai data symbol procedure P, variable i e constant 1 • Dopo la codifica di una sottoespressione, il dizionario è aggiornato usando adattività e predizioni euristiche: ulteriori simboli che descrivono variazioni dell’espressione appena codificata sono aggiunti al dizionario e simboli che si riferiscono a scope lessicali chiusi sono rimossi

  50. Esplicitazione della Fase 2 (2/2) • Es. dopo aver codificato l’espressione i+1, i simboli i-plus-something e something-plus-one potrebbero essere aggiunti; supponiamo di incontrare più avanti nel processo di codifica l’espressione (simile) i+j: questa potrebbe essere rappresentata utilizzando solo due simboli i-plus-something e j.

More Related