1 Laboratorio di Compilatori Contenuti Analisi lessicale Espressioni
Transcript
1 Laboratorio di Compilatori Contenuti Analisi lessicale Espressioni
Politecnico di Torino 1 3 ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ a.a. 1999/2000 Esercitazioni in aula http://www.polito.it/Ulisse/CORSI/INF/N3070/materiale/ Marco Torchiano Dipartimento di Automatica e Informatica Tel. (011 564) 7081 E-mail: [email protected] Contenuti ☛ Analisi lessicale ☛ Analisi sintattica ☛ Regole semantiche ☛ Controllo dei tipi ☛ Ambienti di esecuzione ☛ Codici intermedi ☛ Tecniche per il parsing Analisi lessicale ☛ Espressioni regolari ☛ Lex Laboratorio di Compilatori 2 Laboratorio di Compilatori a.a 1999/2000 4 Espressioni regolari in Lex Azioni associate alle espressioni regolari La struttura di un programma sorgente Lex Il codice generato da Lex Risoluzione delle ambiguità Dipendenza dal contesto Condizioni iniziali Eliminazione dei commenti Il file prodotto Inclusione di file Meccanismi di chiamata e definizione di simboli Espressioni regolari ☛ Costituiscono un metodo semplice ed efficace per descrivere insiemi di stringhe di caratteri. ☛ Opportuni operatori consentono di indicare ✏ caratteri ✏ classi di caratteri ✏ opzionalità ✏ ripetizione (0 o più volte) ✏ ripetizione (1 o più volte) ✏ alternativa ✏ concatenazione ✏ ambito di operatori Versione 1.2 © 2000 Marco Torchiano ‘c’ o anche c [a,b,c] o [a-c] exp ? exp * exp + exp1 | exp2 exp1 exp2 ( exp ) 1 Politecnico di Torino 5 Esempi di espressioni regolari 7 ✏ [0-9]+ ☛ numero intero positivo senza 0 inziali ✏ [1-9][0-9]* ☛ numero intero positivo o negativo [0-9]+ ☛ numero in virgola mobile ✏ (‘+’|’-’)? ( ([0-9]+ ‘.’ [0-9]*) ([0-9]* ‘.’ [0-9]+) | ) Espressioni Espressioni regolari regolari gli apici consentono di distinguere un carattere in ingresso (‘+’) da un operatore (+). 6 Esercizi ☛ Scrivere l’espressione regolare che descrive un identificatore nel linguaggio C. ☛ Scrivere l’espressione regolare di un numero in virgola mobile con eventuale parte esponenziale ☛ Come sopra, eliminando zeri iniziali e finali non significativi. ☛ Scrivere l’espressione regolare di un commento nel linguaggio C. ✏ Il commento può contenere i caratteri ‘*’ e ‘/’ purché non adiacenti. Versione 1.2 © 2000 Marco Torchiano lex - un generatore di analizzatori lessicali ☛ Poiché la trasformazione di espressioni regolari in automi a stati finiti deterministici e la implementazione di questi ultimi sono processi meccanici (e noiosi), spesso si utilizza un generatore automatico di analizzatori lessicali. ☛ Lex è un generatore che accetta in ingresso un insieme di espressioni regolari e di azioni associate a ciascuna espressione e produce in uscita un programma che riconosce tali espressioni. ☛ numero intero positivo ✏ (‘+’|’-’)? Laboratorio di Compilatori a.a 1999/2000 8 Lex Lex Programma Programma C C Espressioni regolari in Lex ☛ Le espressioni regolari descrivono sequenze di caratteri ASCII ed utilizzano un certo numero di operatori: ✏ “ \ [ ] ^ - ? . * + | ( ) $ / { } % < > ☛ Lettere e numeri del testo di ingresso sono descritti mediante se stessi: ✏ l’espressione regolare val1 rappresenta la sequenza ‘v’ ‘a’ ‘l’ ‘1’ nel testo di ingresso ☛ I caratteri non alfabetici vengono rappresentati in Lex racchiudendoli tra doppi apici, per evitare ambiguità con gli operatori: ✏ l’espressione xyz“++” rappresenta la sequenza ‘x’ ‘y’ ‘z’ ‘+’ ‘+’ nel testo di ingresso. 2 Politecnico di Torino 9 Laboratorio di Compilatori a.a 1999/2000 Espressioni regolari in Lex 11 Espressioni regolari in Lex ...continua... ...continua... ☛ Il carattere di fine riga viene descritto dal simbolo \n. ☛ I caratteri non alfabetici possono essere anche descritti facendoli precedere dal carattere \ ☛ Il carattere di tabulazione viene descritto dal simbolo \t. ☛ L’operatore ? indica l’espressione precedente è opzionale: ✏ l’espressione xyz\+\+ rappresenta la sequenza ‘x’ ‘y’ ‘z’ ‘+’ ‘+’ nel testo di ingresso. ☛ Le classi di caratteri vengono descritte mediante gli operatori []: ✏ ab?c indica sia la sequenza ac che abc. ✏ l’espressione [0123456789] rappresenta una cifra nel testo di ingresso. ☛ L’operatore * indica l’espressione precedente può essere ripetuta 0 o più volte: ☛ Nel descrivere classi di caratteri, il segno - indica una gamma di caratteri: ✏ ab*c indica tutte le sequenze che iniziano per a, terminano per c e hanno all’interno un numero qualsiasi di b. ✏ l’espressione [0-9] rappresenta una cifra nel testo di ingresso. 10 Espressioni regolari in Lex 12 Espressioni regolari in Lex ...continua... ☛ Per includere il carattere - in una classe di caratteri, questo deve essere specificato come primo o ultimo della serie: ✏ l’espressione [-+0-9] rappresenta una cifra o un segno nel testo di ingresso. ☛ Nelle descrizioni di classi di caratteri, il segno ^ posto all’inizio indica una gamma di caratteri da escludere: ✏ l’espressione [^0-9] rappresenta un qualunque carattere che non sia una cifra nel testo di ingresso. ☛ L’insieme di tutti i caratteri eccetto il fine riga (new line) viene descritto mediante il simbolo “.”. Versione 1.2 © 2000 Marco Torchiano ...continua ☛ L’operatore + indica l’espressione precedente può essere ripetuta 1 o più volte: ✏ ab+c indica tutte le sequenze che iniziano per a, terminano per c e hanno all’interno almeno un b. ☛ L’operatore espressioni: | indica un’alternativa tra due ✏ ab|cd indica sia la sequenza ab che la sequenza cd. ☛ Le parentesi tonde consentono di esprimere la priorità tra operatori: ✏ (ab|cd+)?ef indica sequenze tipo ef, abef, cdddef. 3 Politecnico di Torino 13 Azioni associate alle espressioni regolari continua... 15 Azioni associate alle espressioni regolari ...continua ☛ Il testo riconosciuto viene accumulato nella variabile yytext, definita come puntatore a caratteri. Operando su tale variabile, si possono definire azioni più complesse. ☛ Il numero di caratteri riconosciuti viene memorizzato nella variabile yyleng, definita come intero. ☛ Esiste un’azione di default che viene eseguita in corrispondenza del testo non descritto da nessuna espressione regolare: il testo non riconosciuto viene ricopiato in uscita, carattere per carattere. Versione 1.2 © 2000 Marco Torchiano La struttura di un programma sorgente Lex ☛ Un file sorgente per lex è composto di tre sezioni distinte separate dal simbolo ‘%%’. ☛ La prima sezione contiene le definizioni e può essere vuota. ☛ La seconda sezione contiene le regole sotto forma di coppie espressione_regolare azione. ☛ Le azioni devono iniziare sulla stessa riga in cui termina l’espressione regolare e ne sono separate tramite spazi o tabulazioni. ☛ La terza sezione contiene le procedure di cui il programmatore intende servirsi: se è vuota, il separatore ‘%%’ viene omesso. ☛ Ad ogni espressione regolare è associata in Lex un’azione che viene eseguita all’atto del riconoscimento. ☛ Le azioni sono espresse sotto forma di codice C: se tale codice comprende più di una istruzione o occupa più di una linea deve essere racchiuso tra parentesi graffe. ☛ L’azione più semplice consiste nell’ignorare il testo riconosciuto: si esprime un’azione nulla con il carattere ‘;’. 14 Laboratorio di Compilatori a.a 1999/2000 16 Definizioni continua... ☛ Per semplificare la gestione di espressioni regolari complesse o ripetitive, è possibile definire identificatori che designano sotto-espressioni regolari. ☛ Ogni riga della prima sezione il cui primo carattere non sia di spaziatura è una definizione: numero [+-]?[0-9]+ ☛ La sotto-espressione così definita può essere utilizzata racchiudendone il nome tra parentesi graffe: {numero} printf(“trovato numero\n”); 4 Politecnico di Torino 17 18 Definizioni Laboratorio di Compilatori a.a 1999/2000 ...continua 19 Risoluzione delle ambiguità lessicali ☛ Le righe che iniziano con spazio o tabulazione sono ricopiate identiche nel file di codice C generato dall’esecuzione di Lex. ☛ Lo stesso avviene per tutti i caratteri compresi tra i delimitatori ‘%{‘ e ‘%}’. ☛ Esistono due tipi di ambiguità lessicali: ☛ Tutte le righe presenti nella sezione delle procedure (la terza) del programma sorgente sono ricopiate nel file C generato da Lex. ☛ Nel primo caso verrà eseguita l’azione associata all’espressione regolare che ha riconosciuto la sequenza più lunga. ☛ Nel secondo caso sarà eseguita l’azione associata all’espressione regolare dichiarata per prima nel file sorgente di lex. Esercizi ☛ Si scriva un programma LEX che dato in ingresso un programma C ne produca in uscita uno equivalente ma privo dei commenti. ☛ Si modifichi il programma dell’esercizio precedente in modo che riconosca le direttive #include e, quando le incontra, segnali un errore e termini l’analisi. Si tenga conto che: ✏ possono comparire degli spazi o tabulazioni, ✏ non interessa verificare la correttezza del path: è un controllo che dovrebbe essere effettuato ad un livello superiore. Versione 1.2 © 2000 Marco Torchiano ✏ la parte iniziale di una sequenza di caratteri riconosciuta da un’espressione regolare è riconosciuta anche da una seconda espressione regolare. ✏ La stessa sequenza di caratteri è riconosciuta da due espressioni regolari distinte. 20 Esempio ☛ Dato il file %% for format [a-z]+ {return FOR_CMD;} {return FORMAT_CMD;} {return GENERIC_ID;} e la stringa di ingresso “format”, la procedura yylex ritorna il valore FORMAT_CMD, preferendo la seconda regola alla prima - perché descrive una sequenza più lunga, e la seconda regola alla terza perché definita prima nel file sorgente. 5 Politecnico di Torino 21 Risoluzione delle ambiguità lessicali a.a 1999/2000 23 ☛ Date le regole di risoluzione dell’ambiguità, è necessario definire prima le regole per le parole chiave e poi quelle per gli identificatori. ☛ Il principio di preferenza per le corrispondenze più lunghe può essere pericoloso: ’.*’ {return QUOTED_STRING;} ’first’ quoted string here, ’second’ here riconoscerà 36 caratteri invece di 7. ☛ Una regola migliore è la seguente: ’[^’\n]+’ {return QUOTED_STRING;} Esercizi ☛ Scrivere l’espressione regolare di una stringa che possa contenere al proprio interno anche apici purché preceduti dal carattere ‘\’. ☛ Modificare la regola precedente affinché ritorni una copia della stringa riconosciuta, dopo aver opportunamente rimosso eventuali ‘\’ di troppo. ☛ Scrivere un programma lex che rimuova i tag da un file in formato HTML sostituendo i tag <P> e <BR> con un a-capo. Versione 1.2 © 2000 Marco Torchiano Dipendenza dal contesto ☛ Può essere necessario limitare la validità di un’espressione regolare all’interno di un determinato contesto. ☛ Esistono meccanismi diversi per specificare la dipendenza dal contesto destro (cioè ciò che segue la sequenza di caratteri che si sta riconoscendo) rispetto alla dipendenza dal contesto sinistro (ciò che la precede). ☛ Fa eccezione la gestione del contesto di inizio e fine riga. cerca di riconoscere il secondo apice il più lontano possibile: cosi, dato il seguente ingresso 22 Laboratorio di Compilatori 24 Contesto di inizio e fine riga ☛ Il carattere ‘^’ all’inizio di un’espressione regolare indica che la sequenza descritta deve essere posta all’inizio di riga. ☛ Ciò significa che o si è posizionati all’inizio del file di ingresso o che l’ultimo carattere letto è stato un carattere di fine riga. ☛ Il carattere ‘$’ al termine di un’espressione regolare indica che la sequenza descritta deve essere seguita da un carattere di fine riga. ☛ Tale carattere non viene incluso nella sequenza riconosciuta, deve essere riconosciuto da un’altra regola. 6 Politecnico di Torino 25 Dipendenza dal contesto destro 27 ☛ L’operatore binario ‘/’ separa un’espressione regolare dal suo contesto destro. ☛ Pertanto, l’espressione ab/cd indica la stringa “ab”, ma solo se seguita da “cd”. Dipendenza dal contesto sinistro ☛ È utile poter avere diversi insiemi di regole lessicali da applicare in porzioni diverse del file di ingresso, in genere in funzione di ciò che precede, ovvero del contesto sinistro. ☛ Esistono tre approcci distinti per affrontare il problema: ✏ uso di variabili flag. ✏ uso di condizioni iniziali sulle regole o stati. ✏ uso congiunto di più analizzatori lessicali. Versione 1.2 © 2000 Marco Torchiano Uso di variabili flag ☛ Per esprimere la dipendenza dal contesto sinistro è possibile utilizzare variabili flag nelle azioni delle regole. ☛ Tali variabili devono essere state precedentemente dichiarate come variabili globali. ☛ La funzione REJECT può essere utilizzata per evitare corrispondenze non volute: redirige lo scanner ☛ I caratteri che formano il contesto destro vengono letti dal file di ingresso ma non introdotti nel testo riconosciuto. A tale scopo viene utilizzato un apposito buffer fornito da Lex. ☛ L’espressione ab$ è equivalente a ab/\n. 26 Laboratorio di Compilatori a.a 1999/2000 ✏ alla seconda miglior regola che riconosce lo stesso input, oppure ✏ ad una regola che riconosca un prefisso dello stesso input. 28 Esempio ☛ Il seguente programma gestisce pseudo-commenti del tipo // $var+ int flag=0; %% “//” {flag=1; /* begin of comment */} \n {flag=0; /* \n terminates comment */} “ “ /* ignore blanks*/ \t /* and tabs */ \$[a-zA-Z]+[-+] { if(flag==1) process(yytext); else REJECT;} ... other rules 7 Politecnico di Torino 29 Uso di condizioni iniziali sulle regole (stati inclusivi) 31 %s comment %% <comment>\$[a-zA-Z]+[-+] {process(yytext);} “//” {BEGIN(comment);} \n {BEGIN(INITIAL);} “ “ /* ignore blanks*/ \t /* and tabs */ ... other rules sono attive solo quando l’analizzatore si trova nello stato state. ☛ Gli stati possibili devono essere definiti nella sezione delle dichiarazioni mediante la parola-chiave %s. ☛ Lo stato di default è lo stato 0 o INITIAL. ☛ Si entra in uno stato quando viene eseguita l’azione BEGIN(state); Uso di condizioni iniziali sulle regole (stati inclusivi) continua ☛ Quando uno stato viene attivato, le regole dello stato si aggiungono (or-inclusivo) alle regole base dell’analizzatore. ☛ Tale stato rimane attivo fino a che non se ne attiva un altro. Per tornare alla situazione iniziale si deve eseguire il comando BEGIN(0); oppure BEGIN(INITIAL); ☛ Una regola può essere preceduta dal nome di più stati, separati da virgola, per indicare che tale regola è attiva in ciascuno di essi. Versione 1.2 © 2000 Marco Torchiano Esempio ☛ Il seguente programma gestisce pseudo-commenti del tipo // $var+ ☛ Le regole la cui espressione regolare inizia con <state> 30 Laboratorio di Compilatori a.a 1999/2000 32 Uso congiunto di più analizzatori lessicali (stati esclusivi) ☛ È possibile raggruppare un insieme di regole all’interno di uno stato esclusivo. ☛ Quando l’analizzatore si trova in uno stato esclusivo: ✏ le regole di default risultano disabilitate, ✏ sono attive solo le regole esplicitamente attivate nello stato. ☛ In tal modo è possibile specificare dei “minianalizzatori” che esaminano porzioni particolari del flusso di caratteri in ingresso, quali ad esempio i commenti o le stringhe. ☛ La parola chiave %x introduce uno stato esclusivo. 8 Politecnico di Torino 33 Eliminazione dei commenti 35 ☛ Un possibile approccio all’eliminazione dei commenti del linguaggio C: %% "/*" 34 Eliminazione dei commenti ☛ Questo è un analizzatore che riconosce e scarta commenti del linguaggio C, mantenendo un conteggio del numero della riga di ingresso. int line_num = 1; %x comment %% ... altre regole Versione 1.2 © 2000 Marco Torchiano ++line_num; BEGIN(comment); ; ; ++line_num; BEGIN(INITIAL); Esercizi ☛ Si modifichi il programma precedente affinché consenta l’eliminazione di commenti annidati. ☛ Si estenda il programma precedente in modo che gestisca anche possibili pseudo-commenti della forma $var[+-] ☛ Al programma precedente aggiungere l’eliminazione di commenti brevi, introdotti dal simbolo // e terminati dal carattere di fine riga. { int c; for ( ; ; ) { while ((c = input())!='*' && c != EOF ) ; /* eat up text of comment */ if ( c == '*') { while ((c=input())=='*') ; if ( c == '/' ) break; /* found the end */ } if ( c == EOF ) { error( "EOF in comment" ); break; } } } \n "/*" <comment>[^*\n]* <comment>"*"+[^*/\n]* <comment>\n <comment>"*"+"/" Laboratorio di Compilatori a.a 1999/2000 36 La struttura del programma generato da Lex ☛ Lex produce un programma C, privo di main() il cui punto di accesso è dato dalla funzione int yylex(). ☛ Tale funzione legge dal file yyin e ricopia sul file yyout il testo non riconosciuto. ☛ Se non specificato diversamente nelle azioni (tramite l’istruzione return), tale funzione termina solo quando l’intero file di ingresso è stato analizzato. ☛ Al termine di ogni azione l’automa si ricolloca sullo stato iniziale pronto a riconoscere nuovi simboli. 9 Politecnico di Torino Ingresso e uscita 37 Laboratorio di Compilatori a.a 1999/2000 39 Il file prodotto continua ☛ Negli analizzatori costruiti tramite il programma flex, per motivi di efficienza, vengono letti blocchi di caratteri dal file di ingresso invece che singoli valori. Questo comportamento può essere modificato alterando la definizione della macro YY_INPUT. ☛ Tale macro è definita come segue: ☛ Per default, i file yyin e yyout sono inizializzati rispettivamente a stdin e stdout. ☛ Il programmatore può alterare questa situazione reinizializzando tali variabili globali. YY_INPUT(buf,result,max_size) ☛ Essa pone nel vettore di caratteri buf un blocco la cui lunghezza è compresa tra 1 e max_size. ☛ La variabile intera result contiene il numero di caratteri letti oppure la costante YY_NULL per indicare la fine del file di ingresso. Il file generato 38 yylex() trovato EOF scansione del file di ingresso azione esegue return 1 inizializzazione del file yyin (default=stdin) 40 2 elaborazione del simbolo Versione 1.2 © 2000 Marco Torchiano no si reinizializzazione del file yyin 3 continua ☛ Per far avvenire la lettura un carattere alla volta si può utilizzare il codice seguente: yywrap() altri file? Il file prodotto fine dell’analisi %{ #undef YY_INPUT #define YY_INPUT(buf,result,max_size) \ {\ int c = getchar(); \ result = (c == EOF) ? YY_NULL : (buf[0] = c, 1); \ } %} ☛ Questo non si deve fare per gli analizzatori scritti con Lex, in cui la routine input() richiama direttamente la routine getchar(). 10 Politecnico di Torino 41 Il file prodotto continua 43 ☛ Quando l’analizzatore incontra la fine del file di ingresso, chiama la funzione yywrap(). ☛ Se essa ritorna il valore 0, Lex assume che il file yyin sia stato re-inizializzato e procede con l’analisi. ☛ Altrimenti, l’analizzatore assume che non ci siano altri file da scandire e ritorna al chiamante restituendo il valore 0. ☛ La funzione yywrap() può essere usata per stampare tabelle e statistiche circa l’analisi appena terminata. Su molti sistemi, per default, yywrap() restituisce sempre il valore 1. 42 Regole di fine file ☛ La regola speciale <<EOF>> introduce le azioni da intraprendere alla fine del file quando yywrap restituisce non-zero. ☛ Con tale azione è possibile passare ad un altro file o terminare l’esecuzione. ☛ Questa regola può essere utile unitamente alle start condition per intercettare simboli con delimitatori non bilanciati: \” ... <quote><<EOF>> Laboratorio di Compilatori a.a 1999/2000 { BEGIN(quote); } { error(“EOF in string”); } Versione 1.2 © 2000 Marco Torchiano Inclusione di file ☛ Negli analizzatori generati tramite il programma flex, dove il testo in ingresso è fortemente bufferizzato, è necessario adottare alcuni accorgimenti per sospendere l’elaborazione del file di ingresso al fine di scandire il contenuto di un secondo file che si intende includere nel primo. ☛ Per gestire questo tipo di situazioni, viene fornito un meccanismo per creare ed attivare nuovi buffer di ingresso. 44 Inclusione di file continua ☛ La funzione YY_BUFFER_STATE yy_create_buffer(FILE *file, int size) crea un nuovo buffer, di dimensione size, relativo al file file. ☛ La funzione void yy_switch_to_buffer(YY_BUFFER_STATE new_buf) sostituisce il buffer attuale di ingresso con il buffer new_buf. ☛ Si cancella un buffer con la chiamata void yy_delete_buffer( YY_BUFFER_STATE buf) 11 Politecnico di Torino 45 Inclusione di file continua 47 ☛ Esempio di analizzatore che elabora file inclusi con annidamento. Inclusione di file <incl>[ \t]* continua /* eat the whitespace */ <incl>[^ \t\n]+ {/* process filename */ if (include_stack_ptr>=MAX_INCLUDE_DEPTH) { fprintf( stderr, "Nesting too deep" ); exit( 1 ); } include_stack[include_stack_ptr++]= YY_CURRENT_BUFFER; yyin = fopen( yytext, "r" ); if ( ! yyin ) error( ... ); yy_switch_to_buffer( yy_create_buffer(yyin,YY_BUF_SIZE)); BEGIN(INITIAL); } Versione 1.2 © 2000 Marco Torchiano Inclusione di file continua %% int yywrap() { if ( --include_stack_ptr < 0 ) return(1); else { yy_switch_to_buffer( include_stack[include_stack_ptr]); return(0); } } %x incl %{ #define MAX_INCLUDE_DEPTH 10 YY_BUFFER_STATE include_stack[MAX_INCLUDE_DEPTH]; int include_stack_ptr = 0; %} %% include BEGIN(incl); [a-z]+ ECHO; [^a-z\n]*\n? ECHO; 46 Laboratorio di Compilatori a.a 1999/2000 48 Uso della funzione yylex() ☛ Per un normale uso all’interno di un compilatore, è opportuno che la funzione yylex() restituisca il controllo ogni volta che incontra un identificatore. ☛ A tale scopo è necessario assegnare a ciascuna classe di simboli che si intende riconoscere un opportuno valore. ☛ Poiché tale valore deve essere conosciuto anche dal riconoscitore sintattico, è pratica comune creare un file di definizioni dei simboli del linguaggio ed includere tale file nella sezione delle dichiarazioni. ☛ I valori assegnati sono interi, in genere > 256. 12 Politecnico di Torino 49 Lex: parametri ed opzioni 51 ☛ Lex viene invocato con la seguente sintassi: ☛ Si ottiene come risultato il file lex.yy.c. ☛ Tra le opzioni, le più comuni sono: 50 -t copia il programma generato su stdout, -v stampa una statistica circa il programma compilato, -p stampa informazioni sulle performance dello scanner, -ooutput genera il file output invece di lex.yy.c , -B(-I) genera uno scanner Batch (Interattivo), -f ottimizza le prestazioni dello scanner, -i genera uno scanner case insensitive, -d attiva i messaggi di debug nello scanner. Linguaggi CF e Riconoscitori ☛ Grammatiche CF ✏ Alberi di derivazione ✏ Ambiguità ☛ Parser bottom-up ☛ Introduzione a YACC ✏ ✏ ✏ ✏ Definizione dei simboli Codifica della grammatica Integrazione con l’analizzatore lessicale Formato del programma prodotto da YACC ☛ Ambiguità e conflitti ✏ Conflitti shift-reduce e reduce-reduce ✏ Definizione di operatori e gestione delle priorità in YACC ✏ Gestione degli errori sintattici Versione 1.2 © 2000 Marco Torchiano Grammatiche context free continua... ☛ Il concetto di ricorsione è fondamentale nella scrittura di grammatiche context-free. ☛ È lo strumento per descrivere delle sequenze lunghe a piacere. ☛ Lista (grammatica ricorsiva sinistra) : lex [opzioni] file.lex ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ Laboratorio di Compilatori a.a 1999/2000 ✏ List → List ‘,’ Element ✏ List →=Element ☛ Lista (grammatica ricorsiva destra) : ✏ List → Element Tail ✏ Tail →==’,’ == Element Tail ✏ Tail →==ε ==ε 52 Alberi di derivazione ☛ Data una sequenza di simboli che appartiene al linguaggio definito da una data grammatica, è possibile definire l’albero di derivazione dell’espressione nel seguente modo: ✏ la radice dell’albero è il simbolo iniziale della grammatica ✏ ciascun nodo dell’albero è etichettato con un simbolo della grammatica ✏ le foglie dell’albero sono etichettate con simboli terminali ✏ la sequenza delle foglie, lette da sinistra a destra, corrisponde alla sequenza di ingresso ✏ un nodo n domina una sequenza ordinata di nodi n1, ..., nm, se e solo se la grammatica contiene la regola n → n1 ... nm 13 Politecnico di Torino 53 Alberi di derivazione 55 List → el Tail Tail →==’,’ == el Tail Tail →==ε ==ε List → List ‘,’ el List →=el if (i=1) then if (j=2) then a:=0 else a:=1 data la grammatica: Tail List ✏ ✏ ✏ ✏ ✏ ✏ Tail List Tail 54 ‘,’ el ‘,’ el el ‘,’ el ‘,’ el ε Costrutti Pascal-like ☛ Un frammento di programma di un linguaggio simile al Pascal può essere definito dalle seguenti regole: ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ P → ‘begin’ Is ‘end’ ‘.’ Is → Is I | ε I → ‘if’ C ‘then’ I ‘else I ‘;’ I → ‘while’ C ‘do’ I’;’ I → ‘repeat’ Is ‘until’ C ‘;’ I →==var ‘:=‘ E ‘;’ == I →==‘begin’ == Is ‘end’ C → E Op E Op → ‘=‘ | ‘>‘ | ‘<‘ | ‘>=‘ | ‘<=‘ | ‘~=‘ E → algebric expression Versione 1.2 © 2000 Marco Torchiano Ambiguità ☛ Una grammatica si dice ambigua se esiste almeno una sequenza di simboli del linguaggio per cui esistono due o più alberi di derivazione distinti. ☛ Esercizio: trovare gli alberi di derivazione per List List el Laboratorio di Compilatori a.a 1999/2000 56 S→I I → ‘if’ C ‘then’ I I → ‘if’ C ‘then’ I ‘else I I →=var ‘:=‘ E C → ‘(‘ E ‘=‘ E ‘)’ E → algebric expression Espressioni algebriche ☛ La grammatica che descrive le espressioni algebriche è context-free ed usa la ricorsione: ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ ✏ S→E E → E ‘+’ T E → E ‘-’ T E→T T → T ‘*’ F T → T ‘/’ F T→F F → ‘(‘ E ‘)’ F → number 14 Politecnico di Torino 57 Esempio 59 58 ✏ ✏ ✏ ✏ I →=I1 I1 → ‘if’ C ‘then’ I1 I1 → ‘if’ C ‘then’ I2 ‘else’ I1 I1 →=I3 I2 → ‘if’ C ‘then’ I2 ‘else’ I2 I2 →=I3 I3 →=var ‘:=‘ E Esercizi List id_list List ε id_list id_list ‘;’ id id_list id List List ‘;’ id List id List ε ☛ Una lista , eventualmente vuota, di identificatori terminati da “;” ✏ List List id ‘;’ ✏ List ε 60 ☛ Scrivere due grammatiche di liste di 0 o più identificatori aventi “;” l’una come terminatore, l’altra come separatore. ☛ Scrivere una grammatica non ambigua che descriva le espressioni logiche e che assegni l’opportuna priorità agli operatori ‘and’, ‘or’ e ‘not’. ☛ Costruire la grammatica per i test con un delimitatore alla fine del costrutto e verificare che non è ambigua sull’esempio: Separatori e terminatori ☛ Una lista, eventualmente vuota, di identificatori separati da “;” ☛ I simboli T e F della grammatica algebrica servono a togliere l’ambiguità sulla priorità degli operatori ‘+’ e ‘-’ rispetto agli operatori ‘*’ e ‘/’. ☛ La grammatica per i costrutti del tipo if-then-else può essere resa non ambigua come segue: ✏ ✏ ✏ ✏ ✏ ✏ ✏ Laboratorio di Compilatori a.a 1999/2000 Riconoscitori e analizzatori sintattici ☛ Data una grammatica non ambigua ed una sequenza di simboli in ingresso, un riconoscitore è un programma che verifica se la sequenza appartiene o no al linguaggio definito dalla grammatica. ☛ Un analizzatore sintattico (parser) è un programma che è in grado di associare alla sequenza di simboli in ingresso il relativo albero di derivazione. ☛ Gli algoritmi di analisi possono essere ✏ top-down (dalla radice alle foglie) ✏ bottom-up (dalle foglie alla radice). if C then if C then A end else A end Versione 1.2 © 2000 Marco Torchiano 15 Politecnico di Torino 61 Riconoscitori Bottom-Up 63 La tecnica shift-reduce continua… ☛ Si usa uno stack, inizialmente vuoto, per memorizzare i simboli già riconosciuti. ☛ La sequenza dei simboli sullo stack, seguita dai simboli in ingresso non ancora trattati costituisce sempre una forma sentenziale destra (se l’input è corretto). ☛ I token vengono immessi sullo stack (azione di shift), fino a che la cima dello stack non contiene un handle: quando ciò avviene, l’handle viene ridotto (reduce) con il non-terminale relativo. ☛ Si ha successo se esaminati tutti i simboli in ingresso, lo stack contiene solo il simbolo distintivo della grammatica. Versione 1.2 © 2000 Marco Torchiano La tecnica shift-reduce …continua ☛ Per un corretto funzionamento bisogna: ☛ Il problema principale di un analizzatore top-down è decidere quale produzione deve essere usata per espandere un dato simbolo non terminale. ☛ Nei riconoscitori bottom-up il problema è capire quando si sono incontrati tutti i simboli del lato sinistro di una produzione così da poterli sostituire con il relativo non terminale. ☛ Il problema diventa più complesso se la grammatica non è ε-free. ☛ Se la grammatica è LR(k) si può usare un analizzatore shift-reduce. 62 Laboratorio di Compilatori a.a 1999/2000 ✏ comprendere quando si è raggiunta la fine di un handle; ✏ determinare la lunghezza dell’handle; ✏ decidere quale non terminale sostituire all’handle nel caso in cui ci siano più produzioni con lo stesso lato destro. ☛ Si risolve il problema definendo un insieme di stati del parser e calcolando due tabelle ✏ Action Table: dice quale azione deve eseguire il parser (shift, reduce, terminate, error) in funzione dello stato corrente. ✏ Goto Table: descrive in quale stato deve portarsi il parser. ☛ Il modo con cui vengono calcolati stati e tabelle porta a soluzioni diverse, sia in termini di complessità che di completezza. 64 LALR(1) 2 exp S exp . exp exp . ‘+’ T ‘+’ exp exp ‘+’ . T 4 5 T 3 exp T . NUM NUM T exp exp ‘+’ T . 0 S . exp exp . exp ‘+’ T exp . T exp . NUM T NUM . 1 SS :: exp exp exp exp :: exp exp '+' '+'TT || TT TT :: NUM NUM 16 Politecnico di Torino 65 LALR(1) T 0 3 + NUM NUM 4 1 T 2 Laboratorio di Compilatori a.a 1999/2000 67 exp NUM + NUM eof 0 shift, go to state 1 5 LALR(1) T 0 3 + NUM NUM 4 1 T 2 exp NUM + NUM 3 0 reduce reduce (( exp exp TT )) 5 0 NUM + NUM 1 0 T LALR(1) T 0 3 + NUM NUM 4 1 T 2 68 exp NUM + NUM 1 0 eof reduce reduce (( TT NUM NUM)) 5 0 Versione 1.2 © 2000 Marco Torchiano NUM + NUM 2 0 eof NUM + NUM eof shift, go to state 4 T 3 0 eof exp exp NUM NUM + NUM LALR(1) T 0 3 + NUM NUM 4 1 T 2 2 0 5 T exp exp eof NUM 66 eof T 4 2 0 NUM + NUM eof NUM 17 Politecnico di Torino 69 2 + LALR(1) exp T 0 3 NUM NUM 4 1 Laboratorio di Compilatori a.a 1999/2000 71 4 2 0 NUM + NUM eof T 2 + LALR(1) exp T 0 3 NUM NUM 4 1 T shift, go to state 1 5 5 4 2 0 1 4 2 0 T NUM + NUM T NUM + LALR(1) T 0 3 + NUM NUM 4 1 T 2 exp 72 1 4 2 0 NUM + NUM eof reduce reduce (( TT NUM NUM)) 4 T 2 0 5 exp T T NUM NUM 5 4 2 0 Versione 1.2 © 2000 Marco Torchiano NUM + NUM eof 0 exp exp eof NUM 70 eof reduce reduce (( exp exp exp exp ‘+’ ‘+’TT )) 5 exp exp NUM + NUM T NUM 2 0 NUM + NUM eof Introduzione a Yacc ☛ Yacc è un generatore di analizzatori sintattici che trasforma la descrizione di una grammatica contextfree LALR(1) in un programma C che riconosce ed analizza la grammatica stessa. ☛ Oltre alle regole sintattiche, è possibile specificare quali azioni devono essere eseguite in corrispondenza del riconoscimento dei vari simboli della grammatica. ☛ È necessario integrare il parser così generato con un analizzatore lessicale: alcune convenzioni ne semplificano sensibilmente l’integrazione con lo scanner generato da Lex. 18 Politecnico di Torino 73 Il formato del file di ingresso 75 ✏ le dichiarazioni, ✏ le regole, ✏ le procedure ☛ Le sezioni sono separate dal simbolo ‘%%’ ☛ La prima e l’ultima sezione possono essere vuote. ☛ Se l’ultima sezione è vuota, il secondo separatore può essere omesso. ☛ Possono essere introdotti commenti racchiusi dai simboli ‘/*’ e ‘*/’. Dichiarazioni ☛ Un file Yacc inizia con la sezione delle dichiarazioni. ☛ In essa vengono definiti ✏ ✏ ✏ ✏ ✏ i simboli terminali, il simbolo distintivo della grammatica, le regole di associatività e precedenza tra gli operatori, alcune informazioni semantiche, codice C racchiuso tra i simboli ‘%{’ e ‘%}’. ☛ I simboli non terminali della grammatica sono nomi: ✏ un nome è formato da lettere, ‘_’, ‘.’ e cifre (non iniziali). Dichiarazioni continua ☛ La parola chiave ‘%token’ definisce una lista di nomi di terminali, separati tra loro da uno o più spazi. ☛ Tale parola chiave può comparire più volte in questa sezione. ☛ I letterali non sono dichiarati. ☛ La parola chiave ‘%start’ definisce il simbolo distintivo della grammatica. È lecita una sola occorrenza di questa parola chiave. ☛ Non è necessario dichiarare i simboli non terminali tranne quando si vuole associare loro un valore semantico. ☛ Il file di ingresso su cui opera Yacc è formato da tre sezioni: 74 Laboratorio di Compilatori a.a 1999/2000 76 Codifica della grammatica ☛ La sezione delle regole è costituita da una o più regole del tipo: NonTerminale : CorpoDellaRegola ; ☛ dove NonTerminale è un nome, e CorpoDellaRegola è una sequenza di 0 o più nomi o letterali. ☛ Se per un dato non terminale esistono più produzioni, queste possono essere raggruppate tra loro e separate dal carattere ‘|’. ☛ I simboli terminali sono nomi o letterali: ✏ un letterale è un singolo carattere racchiuso tra apici. Versione 1.2 © 2000 Marco Torchiano 19 Politecnico di Torino 77 Esempio 79 Laboratorio di Compilatori Formato del programma generato %token integer %start Expression ☛ Il parser generato fa capo alla funzione int yyparse() %% ☛ Tale funzione ritorna 1 se è stato incontrato un errore nel testo in ingresso, altrimenti ritorna il valore 0. ☛ Bisogna definire il corpo per la funzione void yyerror(char *) Expression Term Factor 78 a.a 1999/2000 : | ; : | ; : | ; Expression ‘+’ Term Term Term ‘*’ Factor Factor che viene invocata quando si incontra un errore. ☛ Inoltre, il programma generato non contiene il main(), che deve essere definito dal programmatore. integer ‘(’ Expression ‘)’ La sezione delle procedure ☛ Tutto ciò che segue il secondo delimitatore ‘%%’ forma la sezione delle procedure. ☛ Questa porzione di file viene ricopiata tale e quale in uscita. ☛ All’interno di tale sezione vengono comunemente posti: ✏ le procedure semantiche usate nel corso dell’analisi, ✏ l’analizzatore lessicale, ✏ il corpo principale del programma. Versione 1.2 © 2000 Marco Torchiano 80 Integrazione con l’analizzatore lessicale ☛ Il parser generato presuppone l’esistenza di una funzione che realizzi l’analisi lessicale. Tale funzione deve restituire un numero intero positivo che definisce il token letto oppure 0, se è stata raggiunta la fine del file di ingresso. ☛ La funzione di analisi lessicale è così definita: int yylex(); ☛ Il valore semantico, eventualmente associato al simbolo terminale, deve essere memorizzato nella variabile yylval. Tale variabile è allocata all’interno di Yacc. 20 Politecnico di Torino 81 Integrazione con l’analizzatore lessicale continua 83 ☛ Parser e scanner devono accordarsi sui valori associati ai token. Tali valori possono essere scelti da Yacc o dal programmatore. ☛ I terminali introdotti come letterali sono associati al codice ASCII del carattere. ☛ Quando si introduce un terminale per mezzo della parola chiave %token, yacc associa a tale simbolo un valore intero maggiore di 256, mediante il costrutto ‘#define’ del pre-processore C. ☛ Se il nome del token è seguito da un numero intero, esso viene interpretato come valore da associare al token stesso. 82 Integrazione con l’analizzatore lessicale continua ☛ Si possono integrare i programmi generati da Lex e da Yacc in due modi: ✏ in fase di compilazione ✏ in fase di link. ☛ Nel primo caso, il file C prodotto da Lex (il file ‘lex.yy.c’) viene incluso nel programma generato da Yacc mediante la direttiva ‘#include’ posta nella sezione delle procedure. ☛ Ciò rende le definizione dei token visibili direttamente in fase di compilazione. Versione 1.2 © 2000 Marco Torchiano Laboratorio di Compilatori a.a 1999/2000 Esempio: inclusione del sorgente language.y language.y Yacc Yacc #include “lex.yy.c” language.l language.l Lex Lex lex.yy.c lex.yy.c y.tab.c y.tab.c #include “lex.yy.c” include include cc cc parser.obj parser.obj 84 Integrazione con l’analizzatore lessicale continua ☛ Nel secondo caso, si chiede a Yacc (mediante l’opzione ‘-d’) di generare un file (‘y.tab.h’) che contiene le definizioni dei token. ☛ Tale file deve essere incluso dallo scanner in modo che siano definiti i valori dei simboli. ☛ Di conseguenza, il file lex.yy.c, prodotto da Lex, può essere compilato dopo aver generato y.tab.c e y.tab.h tramite Yacc. ☛ Scanner e parser vengono quindi compilati separatamente ed integrati in fase di link. 21 Politecnico di Torino 85 Esempio: compilazione separata language.y language.y Yacc Yacc y.tab.c y.tab.c cc cc Conflitti shift-reduce 87 parser.obj parser.obj S scanner.obj scanner.obj y.tab.h y.tab.h include include Il prossimo simbolo in ingresso è ‘else’. if Possono succedere due cose: 86 Lex Lex E #include “y.tab.h” lex.yy.c lex.yy.c Ambiguità e conflitti in Yacc ☛ Se la grammatica è ambigua, o se non è LALR(1), possono verificarsi dei conflitti. ☛ Ciò significa che l’analizzatore deve scegliere tra più azioni alternative plausibili. ☛ Il problema viene, di solito, risolto modificando la grammatica per renderla non ambigua oppure fornendo indicazioni a Yacc su come comportarsi in caso di ambiguità. ☛ La seconda ipotesi richiede una comprensione adeguata dell’algoritmo di analisi, per evitare di generare comportamenti scorretti. Versione 1.2 © 2000 Marco Torchiano ✏ ridurre le prime quattro voci dello stack, secondo la produzione 1; ✏ introdurre ‘else’ nello stack secondo quanto previsto dalla produzione 2. then cc cc 1) S →==if E then S 2) S →==if E then S else S 3) S →==var = E E if language.l language.l Top of Stack then -d -d #include “y.tab.h” Laboratorio di Compilatori a.a 1999/2000 ☛ In queste situazioni, Yacc decide, in mancanza di altri suggerimenti, di eseguire lo shift. Conflitti reduce-reduce 88 b a Top of Stack 1) S →==a B 2) S →==B 3) B →==a b 4) B →== b Il prossimo simbolo in ingresso è ‘$’. Possono succedere due cose: ✏ ridurre le prime due voci dello stack, secondo la produzione 3; ✏ ridurre la prima voce secondo quanto previsto dalla produzione 4. ☛ In queste situazioni, Yacc decide, in mancanza di altri suggerimenti, di ridurre la regola che è stata definita per prima (la n°3). 22 Politecnico di Torino 89 Definizione degli operatori e gestione delle priorità 91 E →==E == ‘+’ E E →==E == ‘*’ E E →==’(‘ == E ‘)’ E →==integer == ☛ Questa grammatica è fortemente ambigua. 90 Operatori ☛ La regola 1 (così come la 2) è ambigua in quanto non specifica l’associatività dell’operatore ‘+’ (‘*’). ☛ Inoltre le regole 1 e 2 non specificano la precedenza tra gli operatori ‘+’ e ‘*’. ☛ E’ possibile suggerire a Yacc come comportarsi aggiungendo due informazioni nella sezione delle dichiarazioni. ☛ La parola chiave ‘%left’ introduce un operatore associativo a sinistra, ‘%right’ introduce un operatore associativo a destra, ‘%nonassoc’ introduce un operatore non associativo. ☛ L’ordine con cui gli operatori sono dichiarati è inverso alla loro priorità. Versione 1.2 © 2000 Marco Torchiano Regole di risoluzione dell’ambiguità ☛ Ad ogni regola che contiene almeno un terminale definito come operatore, Yacc associa la precedenza ed l’associatività dell’operatore più a destra. ☛ Se la regola è seguita dalla parola chiave ‘%prec’, la precedenza e l’associatività sono quelle dell’operatore specificato. ☛ In caso di conflitto shift-reduce, viene favorita l’azione adatta alla regola con la precedenza maggiore. ☛ Se la precedenza è la stessa, si usa l’associatività: sinistra implica reduce, destra shift. ☛ In certi casi la grammatica può essere resa volutamente ambigua al fine di limitare il numero delle regole. ☛ È necessario però fornire indicazioni sulla risoluzione delle ambiguità. ☛ Un caso tipico è dato dalle espressioni algebriche: 1) 2) 3) 4) Laboratorio di Compilatori a.a 1999/2000 92 Esempio %token integer %left ‘+’ ‘-’ %left ‘*’ ‘/’ %left uminus %% E : E ‘+’ E | E ‘-’ E | E ‘*’ E | E ‘/’ E | ‘-’ E | ‘(’ E ‘)’ | integer ; /* lowest priority */ /* highest priority */ %prec uminus 23 Politecnico di Torino 93 Gestione degli errori sintattici a.a 1999/2000 continua... 95 ☛ In genere quando un parser incontra un errore non dovrebbe terminare brutalmente l’esecuzione ☛ Per default, un parser generato da YACC, quando incontra un errore: ☛ Se il precedente simbolo di look-ahead è accettabile procede nell’analisi. ☛ Altrimenti il parser continua a leggere e scartare simboli finché non ne trova uno accettabile ✏ segnala, tramite yyerror(), un “parse error” ✏ restituisce il valore 1 ☛ Il comportamento di default può essere accettabile per un semplice parser interattivo. Gestione degli errori sintattici ...continua... ☛ Il simbolo predefinito ‘error’ indica una condizione di errore. Esso può essere usato all’interno della grammatica per consentire al parser di riprendere l’esecuzione dopo un eventuale errore. stmts : /* empty statement */ | stmts ‘\n’ | stmts exp ‘\n’ | stmts error ‘\n’ ; Versione 1.2 © 2000 Marco Torchiano Gestione degli errori sintattici ...continua... ☛ Quando il parser generato da Yacc incontra un errore, comincia a svuotare lo stack fino a che incontra uno stato in cui il simbolo ‘error’ è lecito. ☛ Fa lo shift del simbolo error. ✏ Un compilatore in genere cerca di provvedere alla situazione per poter analizzare il resto del file, in modo da segnalare il maggior numero possibile di errori 94 Laboratorio di Compilatori 96 Gestione degli errori sintattici ...continua... ☛ Una semplice strategia per la gestione degli errori è quella di saltare lo statement corrente: stmt : error ‘;’ ☛ A volte può essere utile recuperare un delimitatore di chiusura corrispondente ad uno di apertura: primary : ‘(‘ expr ‘)’ | ‘(‘ error ‘)’ 24 Politecnico di Torino 97 Gestione degli errori sintattici ...continua 99 ☛ Le strategie di recupero degli errori si basano su scommesse; quando si perde la scommessa il rischio è che un errore sintattico ne produca altri spuri. ☛ Per limitare la proliferazione di errori spuri, dopo il riconoscimento di un errore, la segnalazione è sospesa finché non vengono shiftati almeno tre simboli consecutivi. ☛È possibile riattivare immediatamente la segnalazione degli errori utilizzando la macro ‘yyerrok’. 98 Regole semantiche ☛ Traduzione guidata dalla sintassi ☛ La semantica in YACC ✏ ✏ ✏ ✏ ✏ ✏ Definizione di valori semantici Calcolo di attributi sintetizzati Sintesi di attributi in Lex Calcolo di attributi ereditati Trasformazione delle grammatiche Gestione degli errori semantici ☛ Abstract Syntax Tree Versione 1.2 © 2000 Marco Torchiano Laboratorio di Compilatori a.a 1999/2000 Definizioni Guidate dalla Sintassi ☛ Sono una generalizzazione delle grammatiche context-free. ☛ Ad ogni simbolo viene associato un insieme di attributi: ✏ sintetizzati: calcolati in base al valore degli attributi dei figli di un nodo nell’albero di derivazione, ✏ ereditati: calcolati in base al valore degli attributi dei nodi fratelli e del nodo padre nell’albero di derivazione, ☛ Ad ogni produzione viene associato un insieme di regole semantiche che specificano come vengono calcolati gli attributi. 100 Attributi sintetizzati ☛ Se una definizione guidata dalla sintassi usa solo attributi sintetizzati è detta definizione ad attributi S. ☛ È possibile calcolare i valori degli attributi di una definizione ad attributi S bottom-up, dalle foglie alla radice dell’albero di derivazione. E E1 ‘+’ T E.value = E1.value + T.value E T E.value = T.value T number T.value = number.value 25 Politecnico di Torino 101 a.a 1999/2000 Attributi ereditati 103 102 L ‘:’ T ‘;’ L.type = T.type L L1 ‘,’ id L1.type = L.type new_var(id.name,L.type) L id new_var(id.name,L.type) T ‘integer’ T.type = type_int Attributi L ☛ L’ordine di valutazione degli attributi dipende dall’ordine con cui vengono creati o visitati i nodi dell’albero di derivazione. ☛ Comunemente i parser seguono l’ordine di una visita in profondità dell’albero. ☛ Una grammatica è detta ad attributi L se è possibile il calcolo dei valori degli attributi con una visita in profondità dell’albero di derivazione. ☛ In tali grammatiche si ha una propagazione delle informazioni da sinistra a destra (dell’albero di derivazione). Versione 1.2 © 2000 Marco Torchiano La semantica in Yacc continua... ☛ Accanto allo stack che contiene i simboli sintattici, c’è un secondo stack che contiene i valori semantici ad essi associati. ☛ Ad ogni regola può essere associata un’azione da eseguirsi ogni qualvolta la regola è applicata nel processo di analisi. ☛ Tale azione provvede ad aggiornare i valori semantici associati a ciascun simbolo. ☛ Un azione è composta da una o più istruzioni di codice C racchiuse tra i simboli ‘{‘ e ‘}’, ed è posta (di norma) al termine della regola. ☛ Sono utili per esprimere la dipendenza di costrutti di un linguaggio dal contesto in cui si trovano. D Laboratorio di Compilatori 104 La semantica in Yacc ...continua ☛ All’interno di ogni azione, si fa riferimento ai valori semantici associati ai successivi simboli del lato destro della regola mediante i nomi ‘$1’, ‘$2’, ‘$3’, ... ☛ Il valore semantico associato al simbolo del lato sinistro della regola è indicato dal nome ‘$$’. ☛ Il traduttore generato da Yacc associa ad ogni regola che non contiene un’azione esplicita la seguente azione di default: { $$ = $1; } 26 Politecnico di Torino 105 Esempio 107 %{ ☛ Per default, ad ogni simbolo viene associato un solo attributo di tipo intero. ☛ Ciò può essere cambiato ridefinendo (mediante typedef) il simbolo YYSTYPE, oppure servendosi, nella sezione delle dichiarazioni, dei costrutti ‘%union’ e ‘%type’ . ☛ Nel primo caso, tutti i simboli sono legati ad un valore semantico il cui tipo è quello attribuito a YYSTYPE. ☛ In alternativa, ‘%union’ consente di definire un insieme di interpretazioni alternative dei valori semantici contenuti nello stack. ☛ ‘%type’ associa una data interpretazione ad uno o più simboli non terminali. Versione 1.2 © 2000 Marco Torchiano typedef struct _function_desc { int arg_number; char *name; } function_desc; %} %union { function_desc function; float expression; char * id; int other; } %type <expression> E T F %type <function> Func %type <other> Args %% /* ... rules here... */ ☛ Un’azione può contenere istruzioni qualsiasi, anche facenti riferimento a variabili globali: A : B C D { printf(“%d\n”,i); } ; Tipi dei valori semantici Esempio ☛ ‘Func’ ha due attributi: un intero ed un puntatore a carattere. I simboli ‘E’, ‘T’ ed ‘F’ hanno un solo attributo di tipo reale, ‘Args’ un solo attributo di tipo intero. ☛ Data la grammatica delle espressioni algebriche, la regola seguente associa al simbolo ‘E’ la somma o la sottrazione dei valori degli addendi/sottraendi: E : E ‘+’ T { $$ = $1 + $3; } | E ‘-’ T { $$ = $1 - $3; } ; 106 Laboratorio di Compilatori a.a 1999/2000 108 Calcolo di attributi sintetizzati ☛ Un attributo sintetizzato viene calcolato assegnando, nell’azione associata ad una data regola, un valore alla pseudo-variabile ‘$$’. ☛ Tale valore è funzione, in genere, di uno o più degli attributi dei simboli del lato destro della regola stessa. ☛ Se ad un simbolo sono stati associati più attributi sotto forma di ‘struct’, è possibile far riferimento a ciascuno di essi con la sintassi $n.attr. Func: identifier ‘(‘ Args ‘)’ { $$.name=$1; $$.arg_number=$3; } ; 27 Politecnico di Torino 109 Sintesi di attributi in Lex 111 ☛ In un riconoscitore bottom-up, non viene riservato spazio sullo stack semantico fino a che il corrispondente simbolo sintattico non è riconosciuto. ☛ Ciò rende problematica la gestione degli attributi ereditati. ☛ Se la grammatica è ad attributi L, e le regole semantiche per il calcolo degli attributi ereditati sono copy-rules è possibile “aggirare l’ostacolo” in maniera semplice. ☛ Se le regole sono più complesse è possibile inserire dei marker, cioè dei non-terminali che si espandono in ε. %token <expression> number ☛ L’analizzatore lessicale deve provvedere a depositare, all’atto del riconoscimento del simbolo, il suo valore semantico nella variabile globale esterna ‘yylval’. ☛ Se si è usato il costrutto %union, nello scanner bisogna citare esplicitamente il campo della unione a cui ci si riferisce: [0-9]+”.”[0-9]* { yylval.field = atof(yytext); return(number);} Esercizi Calcolo di attributi ereditati continua... ☛ Se ad un simbolo terminale sono associati degli attributi, bisogna definirne il tipo nella sezione delle dichiarazioni con la sintassi seguente: 110 Laboratorio di Compilatori a.a 1999/2000 112 Calcolo di attributi ereditati …continua... ☛ Si scriva una grammatica per riconoscere delle espressioni regolari tipo Lex e costruire l’albero di derivazione relativo all’espressione riconosciuta. Si utilizzino le primitive: ✏ treenode* create_node(char* name) ✏ void add_son(treenode* father, treenode* son) ☛ Si scriva una grammatica che riconosca sequenze di espressioni matematiche separate dal simbolo ‘=‘ e che stampi, in conseguenza del riconoscimento di tale simbolo il valore dell’espressione riconosciuta. Versione 1.2 © 2000 Marco Torchiano ☛ Una produzione che usa una copy-rule: (1) (2) Decl D1 Situazione dello stack prima di ridurre D1 T D1 id id.n D1.i = T.s var(D1.i,id.n) $1 contesto della regola (2) T.s ... 28 Politecnico di Torino Calcolo di attributi ereditati 113 Calcolo di attributi ereditati 115 ...continua... Decl T Decl1 type: type:int int type: type:int int id name: name:j j 114 Laboratorio di Compilatori a.a 1999/2000 ☛ Il simbolo ‘Decl1’ ha l’attributo ereditato ‘type’. ☛ Il valore di tale attributo è presente sullo stack semantico (nella posizione associata a ‘T’) prima che venga creato il simbolo ‘Decl1’. ☛ Tuttavia esso è al di fuori del contesto semantico della regola relativa a ‘Decl1’. Calcolo di attributi ereditati ...continua... ☛ È possibile accedere ai valori semantici precedentemente memorizzati nello stack purché se ne conosca la posizione relativa alla regola corrente. ☛ la pseudo-variabile ‘$0’ indica il valore associato al simbolo che precede immediatamente (nel parsing left-to-right rightmost-derive) la regola, ‘$-1’ quello ancora precedente e così via. ☛ Assumendo che il simbolo ‘Decl1’ sia sempre preceduto da un identificatore di tipo: Decl1: id { $$.type = $0.type; add_id($1.name, $$.type); } ; Versione 1.2 © 2000 Marco Torchiano ...continua... Decl T type: type:int int id name: name:i i Decl1 type: type:int int ‘,’ Empty type: type:int int Decl1 type: type:int int id name: name:j j 116 ☛ Aggiungendo la regola Decl1: id ‘,’ Decl1 ; non è più vero che ‘Decl1’ è sempre preceduto da un identificatore di tipo. ☛ Si può aggiungere un nonterminale che porta questa informazione: Decl1: id ‘,’ Empty Decl1 { $$.type= $0.type; add_id($1.name, $$.type); } ; Empty: {$$=$-2;}; Calcolo di attributi ereditati ...continua ☛ Si può evitare di introdurre esplicitamente un non terminale riscritto come stringa nulla, utilizzando, nella parte destra delle regole, le azioni intermedie. ☛ Esse vengono automaticamente sostituite da YACC con un simbolo non terminale, a sua volta riscritto come ε. ☛ Nelle azioni intermedie, ‘$$’ indica il valore associato al non-terminale implicito. ☛ Per associare a tale valore un’interpretazione tra quelle introdotte con il costrutto %union, si usa la sintassi ‘$<union_field>$’. 29 Politecnico di Torino Esempio 117 %{ typedef struct { int type_name; char* id_name; } YYSTYPE; %} %token integer char id %start Decls %% Decls : /* empty */ | Decls Decl ; Decl : TypeName Decl1 ‘;’ ; TypeName : integer { $$.type_name=1; } | char { $$.type_name=2; }; 119 Decl1 : id { $$.id_name = $1.id_name; $$.type_name = $0.type_name } | id ‘,’ {$$ = $0;} Decl1 { $$.id_name = $1.id_name; $$.type_name = $0.type_name; } ; %% Trasformazione della grammatica 118 ☛ È possibile evitare l’uso degli attributi ereditati trasformando la grammatica. D L ‘:’ T D id L T integer | real L ‘,’ id L | ‘:’ T L L ‘,’ id | id T integer | real DD LL LL DD LL TT i , j : integer Versione 1.2 © 2000 Marco Torchiano Laboratorio di Compilatori a.a 1999/2000 LL TT Aggiunta di marker ☛ L’uso di marker, ovvero di simboli non terminali che si espandono in ε è molto utile in varie occasioni; tuttavia può generare alcune complicazioni. ☛ La seguente trasformazioni genera una grammatica che non è LR(1): L 120 Lb|a L M MLb|a ε Gestione degli errori semantici ☛ All’interno delle azioni possono essere eseguiti i vari controlli che verificano la correttezza semantica dei costrutti sintattici (ad esempio, verificano la giusta corrispondenza tra tipi di operandi). ☛ Si possono utilizzare le seguenti macro: ✏ YYABORT: termina l’esecuzione del parser e restituisce 1; ✏ YYACCEPT: termina l’esecuzione del parser e restituisce 0; ✏ YYERROR: genera un errore sintattico (non chiama yyerror). ☛ Alternativamente si può cercare di ricuperare almeno parzialmente l’errore e proseguire nella analisi sintattica. i , j : integer 30 Politecnico di Torino 121 Abstract Syntax Tree 123 ☛ Gli AST sono una forma condensata degli alberi di derivazione. ☛ Gli operatori e le parole chiave non compaiono come foglie dell’albero. ☛ Esse sono associate ai nodi intermedi che sarebbero stati i genitori delle foglie. S S1 ✏ sistemi di tipi, ✏ espressioni di tipi, ✏ costruttori di tipo. ☛ Costruzione di type-expression ☛ Symbol tables ☛ Implementazione di un type-checker ✏ strutture dati ✏ grammatica ✏ semantica S2 Esercizi ☛ Si modifichi la grammatica delle dichiarazioni C in modo che usi solo attributi ereditati. ☛ Si scriva una grammatica per riconoscere delle espressioni regolari tipo Lex e costruire l’AST relativo all’espressione riconosciuta. Si utilizzino le primitive: ✏ ast_node* create_leaf(char ch) ✏ ast_node* create_node(char optr, ast_node* opnd1, ast_node* opnd2) ✏ la seconda primitiva può accettare 1 o 2 operandi, ovvero il secondo operando può essere NULL; ✏ gli operatori sono rappresentati dai caratteri ‘+’ ‘*’ ‘?’ ‘|’ ‘&’, dove l’ultimo rappresenta la concatenazione. Versione 1.2 © 2000 Marco Torchiano Controllo dei tipi ☛ Type expressions if-then-else if B then S1 else S2 B 122 Laboratorio di Compilatori a.a 1999/2000 124 Sistemi di tipi ☛ Il controllo dei tipi è basato su: ✏ i costrutti sintattici del linguaggio, ✏ il concetto di tipo, ✏ le regole per assegnare i tipi ai costrutti. ☛ In generale, i tipi possono essere: ✏ primitivi (int, float, char) ✏ costruiti (struct, union) ☛ Ad ogni costrutto del linguaggio deve essere possibile associare un tipo, descritto per mezzo di una type-expression. 31 Politecnico di Torino 125 Laboratorio di Compilatori a.a 1999/2000 Type-expressions 127 ☛ Una type-expression è formata da un tipo primitivo, oppure è un costruttore di tipo applicato ad una typeexpression. ☛ I tipi primitivi sono tutti quelli necessari al linguaggio, più i due tipi speciali: Costruttori di tipo ☛ Una funzione mappa un elemento del proprio dominio in un elemento del range. ☛ Funzioni: T1 T2 ✏ T1 : tipo del dominio, ✏ T2 : tipo del range. ✏ void : denota l’assenza di un tipo, ✏ type_error : indica un errore rilevato durante il controllo dei tipi. ☛ La funzione int* f(char a, char b) viene rappresentata dalla seguente type expression: ☛ È possibile assegnare un nome ad un tipo o ad un suo campo. ☛ Un nome è una type-expression. (char x char) ☛ Teoricamente una funzione può avere come dominio e codominio tipi qualsiasi: ((int 126 Costruttori di tipo ☛ Array: 128 pointer( T ) T 1 X T2 struct( T ) char v[10] struct { int i; char s[5]; } int) x (int int)) (int int) Grafo dei tipi array( I , T ) ✏ I : dimensione dell’array ✏ T : type expression ☛ Puntatori: ☛ Prodotto: ☛ Strutture: pointer(int) ☛ Un modo efficace di rappresentare expression è l’uso di grafi (alberi o DAG). (char x char) le type pointer(int) array(10,char) X pointer char integer struct((i x int )x( s x array(5,char))) Versione 1.2 © 2000 Marco Torchiano 32 Politecnico di Torino 129 Costruzione di type-expression 131 ☛ Il calcolo dei tipi per il C potrebbe essere fatto tramite la seguente definizione guidata dalla sintassi: Vl.type=T.type D T Vl ‘;’ Vl V V.type=Vl.type Vl Vl1 ‘,’ V V.type=Vl.type V ‘*’ V1 V1.type=pointer(V.type) V Va Va.type=V.type Va Va1 ‘[‘ num ‘]’ Va1.type=array(num.val,Va.type) Va id add_var(id.name,Va.type); 130 Laboratorio di Compilatori a.a 1999/2000 Costruzione di type-expression 132 Costruzione di type-expression D Vl Vl V T Vl ‘;’ V Vl1 ‘,’ V P id A P P ε P1 ‘*’ A A ε A1 ‘[‘ num ‘]’ Vl.type=T.type V.type=Vl.type V.type=Vl.type P.base=V.type A.base=P.type add_var(id.name,A.type) P.type=P.base P.type=pointer(P1.type) P1.base=P.base A.type=A.base A.type=array(num.val,A1.type) A1.base=A.base Costruzione di type-expression D pointer(char) Vl D char pointer(char) Vl V V char Non è ad attributi L V array(10,pointer(char)) Va char * id argv [ Versione 1.2 © 2000 Marco Torchiano A P Va T P T ε num 10 char * ] array(10,pointer(char)) id ε argv [ A num 10 ] ; ; 33 Politecnico di Torino 133 Costruzione di type-expression ☛ Occorre costruire type espressioni: E.type E literal E num E.type E id E.type E ‘&’ E1 E.type E ‘*’ E1 E.type E 134 E1 [ E2 ] Laboratorio di Compilatori a.a 1999/2000 135 expression anche per le ☛ La possibilità di assegnare dei nomi ai tipi complica la verifica dell’equivalenza. ☛ Esempio: = char = integer typedef cell* link; link p; cell* q; = lookup(id.name) = pointer(E1.type) E.type = if E1.type=array(s,T) and E2.type=integer then T else type_error ☛ Il modo più naturale di definire l’equivalenza tra due type-expression è l’equivalenza strutturale. ☛ Due espressioni sono uguali se: ✏ sono lo stesso tipo primitivo, oppure ✏ sono l’applicazione del costruttore di tipo a tipi equivalenti. 136 Riferimenti circolari ☛ Molte strutture dati basilari, come le liste concatenate o gli alberi, sono definite ricorsivamente. ☛ Tale configurazione introduce, potenzialmente, dei cicli nel grafo di tipi. struct cell ☛ Utilizzando la naturale rappresentazione delle type expression come alberi, è possibile utilizzare un algoritmo a discesa ricorsiva per verificare l’equivalenza. X X info Versione 1.2 © 2000 Marco Torchiano type link = ^cell; var p : link; q : ^cell; ☛ Le variabili p e q sono uguali? La risposta varia se viene utilizzata l’equivalenza di nomi o l’equivalenza strutturale. ☛ In C viene utilizzata l’equivalenza strutturale, mentre il Pascal adotta l’equivalenza dei nomi. = if E1.type=pointer(T) then T else type_error Equivalenza Nomi dei tipi 1 X integer next pointer 2 struct cell 34 Politecnico di Torino 137 Riferimenti circolari Laboratorio di Compilatori a.a 1999/2000 139 ☛ Un semplice algoritmo per la verifica dell’equivalenza strutturale di tipi può non funzionare in presenza di riferimenti circolari. ☛ In C è sempre necessario dichiarare un nome prima dell’uso, con l’unica eccezione di puntatori a struct non ancora dichiarate. ☛ Se il nome del record è parte del costruttore di struttura; il test per l’equivalenza si ferma quando raggiunge due strutture: Type checker ☛ Un type-checker è formato da un insieme di moduli interoperanti: ✏ ✏ ✏ ✏ scanner: riconosce il lessico, parser: verifica la sintassi ed aggiunge la semantica, gestore di type-expresssion, gestore di symbol table. Symbol table ✏ se hanno lo stesso nome, sono equivalenti; ✏ altrimenti sono diversi. 138 Esercizi ☛ Si scriva la definizione guidata dalla sintassi per la costruzione della type-expression relativa ad una struct C. ☛ Date le seguenti dichiarazioni C: typedef struct { int a,b; } CELL, *PCELL; CELL foo[100]; PCELL bar(int x, CELL y); si scrivano le type-expression corrispondenti. Versione 1.2 © 2000 Marco Torchiano Parser Type expression Scanner 140 Symbol table ☛ Le tabelle dei simboli associano valori a nomi per rendere accessibili informazioni semantiche, legate ad un identificatore, al di fuori del contesto in cui esso è stato dichiarato. ☛ Le informazioni associate a ciascun nome vengono utilizzate per verificare il corretto uso semantico degli identificatori di un programma. ☛ Tali informazioni possono essere aggiornate dinamicamente via via che nuove caratteristiche del nome sono dichiarate nel programma o dedotte dal compilatore. 35 Politecnico di Torino 141 Symbol table 143 ☛ Le informazioni che vengono memorizzate all’interno di una tabella dei simboli vengono dette entry. ☛ Ogni operazione di inserimento definisce una chiave (solitamente una stringa) tramite la quale poter, successivamente, reperire le informazioni. ☛ In generale, le informazioni memorizzate in una symbol table non sono omogenee, perciò conviene memorizzare dei puntatori invece delle informazioni stesse. ☛ Un traduttore utilizza diverse tabelle dei simboli per memorizzare informazioni diverse o appartenenti a contesti diversi. 142 Symbol table: interfaccia ☛ Una porzione di codice che implementa una tabella dei simboli dovrebbe consentire le seguenti operazioni astratte: ✏ Create() ✏ Destroy() ✏ Enter(K,I) ✏ Lookup(K) Laboratorio di Compilatori a.a 1999/2000 costruttore distruttore aggiunge alla tabella le informazioni I, associandole alla chiave K. cerca nella tabella le informazioni associate alla chiave K. ☛ Nell’ottica del riutilizzo, è conveniente associare ad ogni tabella un handle, che identifica l’istanza della tabella su cui si desidera operare. Versione 1.2 © 2000 Marco Torchiano Symbol table: implementazione ☛ Da un punto di vista implementativo una tabella può essere realizzata con una delle seguenti tecniche ✏ ✏ ✏ ✏ ✏ Liste disordinate Liste ordinate Alberi binari Tabelle hash BTree … ☛ La scelta dipende dal numero di simboli da memorizzare, dalle prestazioni che si intendono ottenere, dalla complessità del codice che si intende produrre. 144 Symbol table: implementazione ☛ L’interfaccia di un modulo per la gestione di symbol table basate su hash-table potrebbe essere: ht_handle ht_create(int size); void ht_destroy(ht_handle); int ht_insert(ht_handle, char* key, void* info); void* ht_lookup(ht_handle, char* key); void* ht_delete(ht_handle, char* key); 36 Politecnico di Torino 145 a.a 1999/2000 Type expression 147 ☛ La rappresentazione naturale delle type expression tramite grafi di tipi può essere trasformata in una rappresentazione interna. ☛ La gestione delle type expression richiede te_node* te_make_base(int code); te_node* te_make_name(char* name); te_node* te_make_product(te_node* l, te_node* r); te_node* te_make_struct(te_node* flds, char* n); te_node* te_make_fwdstruct(char* name); te_node* te_make_function(te_node* d, te_node* r); te_node* te_make_pointer(te_node* base); te_node* te_make_array(int size, te_node* base); void te_cons_struct(te_node* str, te_node* flds); ☛ I nodi devono essere in grado di rappresentare i diversi costruttori di tipo ed i tipi di base. ☛ Le primitive servono per nascondere la rappresentazione interna dei nodi e consentire all’utilizzatore di scrivere del codice il più semplice possibile. Type expression: implementazione Type expression: implementazione ☛ Il modulo di gestione delle TE deve offrire le seguenti primitive: ✏ la definizione della struttura dati dei nodi del grafo, ✏ la definizione delle primitive per operare sui nodi. 146 Laboratorio di Compilatori 148 Type checker: semantica ☛ Ogni nodo di un grafo dei tipi contiene: ✏ un tag, che rappresenta il tipo di nodo; ✏ un campo fisso, che rappresenta il primo operando; ✏ una union di vari campi typedef struct te_node_tag { int tag; struct te_node_tag* left; union { struct te_node_tag* right; int size; int code; char* name; } r; } te_node; Versione 1.2 © 2000 Marco Torchiano ☛ Oltre alle funzioni offerte dal modulo di gestione delle type expression, è opportuno fornire alcune primitive per l’accesso alle tabelle dei simboli. te_node* type_lookup(char* ); int add_type(char* name,te_node* type); int add_var(char* name,te_node* type); ☛ Oltre a semplificare la scrittura delle azioni semantiche, permettono di nascondere i dettagli implementativi delle tabelle. 37 Politecnico di Torino 149 Laboratorio di Compilatori a.a 1999/2000 Type checker: semantica Type checker: semantica 151 YACC ☛ L’organizzazione delle tabelle dei simboli può seguire due strade. ☛ Due tabelle separate: ✏ per i tipi, definiti con typedef o struct tag, ✏ per le variabili. ✏ il “genere” del simbolo, ✏ le informazioni. %type %type %type %type %type typedef struct { int symbol_kind; char* name; te_node* type; } table_entry; Type checker: semantica Decl : T Vlist ; { } T : TYPE ; { $$=$1; } Vlist V :V | Vlist ',' { $$=$<type>0 } V ; : Ptr ID {$$=$1;} Ary { add_var($2,$4); } ; Ptr : /* empty */ | Ptr '*' ; { $$=$<type>0; } { $$=te_make_pointer($1); } Ary : /* empty */ | Ary '[' NUM ']' ; { $$=$<type>0; } { $$=te_make_array($3,$1); } Versione 1.2 © 2000 Marco Torchiano ID [A-Za-z_][0-9A-Za-z_]* %% {ID} { te_node* pte; pte = type_lookup(yytext); if(pte!=NULL){ yylval.type=pte; return TYPE; } yylval.name=strdup(yytext); return ID; %token <ival> NUM %token <name> ID %token <type> TYPE ☛ Un’unica tabella in cui vengono memorizzati: 150 LEX %union { te_node* type; char* name; int ival; } <type> <type> <type> <type> <type> Vlist V T Ptr Ary } Type checker: grammatica 152 S : /* empty */ | S Decl ';' ; Decl T : | | | ; SFL : T Vlist | TYPEDEF T Vlist ; TYPE STRUCT ID '{' SFL '}' STRUCT '{' SFL '}' STRUCT ID : Field | SFL Field ; Field : T Vlist ; Vlist : V | Vlist ',' V ; V : Ptr ID Ary ; Ptr : /* empty */ | Ptr '*' ; Ary : /* empty */ | Ary '[' NUM ']' ; 38 Politecnico di Torino 153 Type checker: semantica 155 ✏ una variabile, ✏ un campo di una struttura, ✏ un nome di un nuovo tipo definito dall’utente. ☛ Volendo utilizzare la grammatica precedente, occorre inserire, nella semantica di V, tre alternative per trattare i diversi casi. ☛ Per distinguere i casi occorre impostare una variabile di stato in corrispondenza: ✏ del riconoscimento della keyword typedef, oppure ✏ della keyword struct seguita da graffe. Ambienti di esecuzione ☛ Procedure e parametri ✏ Parametri formali ed attuali ✏ Record di attivazione ☛ Tabelle a blocchi ☛ Allocazione della memoria ☛ Passaggio di parametri ✏ Stack frame ✏ Tail recursion Versione 1.2 © 2000 Marco Torchiano Procedure ed attivazioni ☛ Un programma può essere visto come un insieme di procedure o funzioni; essenzialmente, una procedura assegna un identificatore (nome) ad un insieme di statement, (corpo). ☛ È bene distinguere il codice sorgente di una procedura dalla sua attivazione all’interno di un processo. ☛ Si ha una chiamata ad una procedura quando il suo nome compare in uno statement eseguibile. ☛ Quando viene eseguita la chiamata, la procedura viene attivata cioè vengono eseguiti gli statement del suo corpo. ☛ Il non terminale V può corrispondere a: 154 Laboratorio di Compilatori a.a 1999/2000 156 Parametri ☛ Nella definizione di una procedura possono comparire dei parametri formali. ☛ All’atto della chiamata, vengono specificati degli argomenti o parametri attuali, corrispondenti ai parametri formali. ☛ Ad ogni attivazione di una procedura vengono forniti dei parametri formali. ☛ Una procedura è ricorsiva se una sua attivazione può iniziare prima che ne finisca una precedente. ☛ È possibile la ricorsione solo se ad ogni attivazione i parametri attuali vengono memorizzati in locazioni diverse. 39 Politecnico di Torino 157 Stack di controllo a.a 1999/2000 159 ☛ Ad ogni attivazione di una procedura occorre associare un record di attivazione che contiene le informazioni utilizzate dal codice nell’attivazione corrente. ☛ Risulta naturale memorizzare i record di attivazione in uno stack di controllo. ☛ È possibile associare l’ambito di visibilità (scope) legato ad una procedura al suo record di attivazione. Come conseguenza, ogni attivazione ha le proprie variabili locali. ☛ Il concetto di scope ha un’applicazione più ampia del semplice contesto di una procedura. 158 Tabelle strutturate a blocchi ☛ Nei linguaggi strutturati è possibile introdurre più identificatori con lo stesso nome, in contesti separati e/o nidificati. ☛ Gli identificatori sono racchiusi in unità (moduli, procedure, blocchi, ecc.) che definiscono un contesto (scope). ☛ Data un’istruzione, l’unità più interna che la racchiude viene detta contesto corrente. ☛ Tutte le unità che racchiudono lo scope corrente sono dette contesti aperti. ☛ Le restanti unità del programma formano i contesti chiusi. Versione 1.2 © 2000 Marco Torchiano Laboratorio di Compilatori Regole di visibilità ☛ Per ciascun punto di un programma, solo i nomi dichiarati nel contesto corrente o nei contesti aperti sono accessibili. ☛ Se un nome è dichiarato in più di un contesto, la dichiarazione più interna (più vicina al contesto corrente) viene utilizzata per interpretare un’occorrenza del nome. ☛ Possono essere fatte nuove dichiarazioni solo nel contesto corrente. 160 Tabelle a blocchi: realizzazione ☛ Le regole di visibilità suggeriscono la realizzazione di un’insieme di symbol table organizzate a stack. ☛ La tabella in cima allo stack corrisponde al contesto corrente; quella successiva al contesto immediatamente più esterno, e cosi via. ☛ Dato un simbolo, esso viene cercato dapprima in cima allo stack e, se non trovato, via via nelle tabelle successive. ☛ Ogni volta che si apre un nuovo contesto, una tabella vuota viene inserita in cima allo stack. ☛ Alla chiusura del contesto, tale tabella viene rimossa dallo stack. 40 Politecnico di Torino 161 Tabelle a blocchi: interfaccia a.a 1999/2000 163 ☛ Si può realizzare un modulo che incapsula il comportamento a stack delle tabelle: inserisce una tabella in cima allo stack, essa diventa la tabella del contesto corrente, ✏ Pop() rimuove la tabella del contesto corrente, ✏ CurrentScope() restituisce la tabella corrente, ✏ Lookup(Id) cerca nello stack l’elemento Id ritorna un riferimento all’oggetto. ☛ All’atto della creazione di una nuova tabella, è necessario memorizzarne un riferimento, per poter accedere in seguito alle informazioni in essa contenute. Estensioni per i record ☛ Insieme alla definizione di un record (struttura, unione, classe) è necessario memorizzare una tabella dei simboli riportante le informazioni relative ai singoli campi. ☛ Quando il compilatore incontra un riferimento ad una variabile di tipo record, ottiene dalla tabella dei simboli dei tipi un puntatore alla tabella dei campi del record. ☛ Mediante tale puntatore determina le caratteristiche del campo selezionato. Versione 1.2 © 2000 Marco Torchiano Dichiarazioni implicite ☛ In alcuni linguaggi non è necessaria una dichiarazione esplicita di un simbolo prima del suo uso. ☛ In questi casi, quando il simbolo viene incontrato il compilatore deve dedurre dal contesto il maggior numero di informazioni possibili ed inserirle nella tabella dei simboli. ☛ Se il linguaggio ammette anche contesti annidati, bisogna determinare se il simbolo appartiene ad un contesto esistente o costituisce una nuova definizione nel contesto corrente. ✏ Push(Table) 162 Laboratorio di Compilatori 164 Riferimenti in avanti ☛ Un caso particolare di dichiarazione implicita è dato dall’uso dei nomi delle etichette di salto (label) in alcuni linguaggi. ☛ Quando il compilatore incontra un riferimento all’etichetta, essa può non essere ancora stata definita. ☛ Non è possibile, in tal caso, creare dei compilatori a passo singolo: i riferimenti a entità sconosciute sono registrati temporaneamente e, al termine dell’analisi, sostituiti con i relativi indirizzi. Solo dopo può essere generato il codice. 41 Politecnico di Torino 165 Laboratorio di Compilatori a.a 1999/2000 Integrazione con il parser 167 ☛ Nello scrivere la grammatica diventa necessario introdurre azioni semantiche che interagiscano con la tabella dei simboli per la corretta interpretazione degli identificatori. ☛ Tali azioni possono essere intermedie rispetto a qualche regola (in particolare con i record). In tal caso la grammatica può diventare non LALR(1) e, come tale, non essere utilizzabile con generatori automatici come YACC. ☛ La seguente dichiarazione può essere una variabile oppure una funzione: Allocazione della memoria ☛ All’interno di un programma possono essere utilizzate, anche contemporaneamente, diverse strategie di allocazione della memoria. ☛ Il principale aspetto distintivo è costituito dal momento in cui viene assegnata una locazione di memoria ad una variabile. ☛ In C si definiscono tre grandi classi di allocazione: ✏ statica: a tempo di compilazione ✏ automatica: all’attivazione di una procedura. ✏ dinamica: in qualsiasi momento durante l’esecuzione. pippo (x); 166 Dipendenza dal contesto ☛ La semantica associata ad un costrutto sintattico può variare in funzione del contesto in cui esso si trova. ☛ Per utilizzare generatori di parser context-free occorre introdurre, nel parser, uno stato (semantico) che modifichi l’interpretazione dei costrutti sintattici. ☛ Questo tipo di soluzione può creare problemi se si utilizzano regole per il recupero degli errori. ☛ Occorre ripristinare lo stato precedente al riconoscimento del costrutto sintattico in cui si intercetta l’errore. Versione 1.2 © 2000 Marco Torchiano 168 Variabili statiche char* char* s1="pippo"; s1="pippo"; char char s2[]="pluto"; s2[]="pluto"; .globl s1 char* .section .rodata char* s3(){ s3(){ return return "minni"; "minni"; .LC0: }} .string "pippo" .data s1: .long .LC0 .globl s2 s2: .string "pluto" .section .rodata .LC1: .string "minni" *s1=‘a’; *s3()=‘a’; *s2=‘a’; 42 Politecnico di Torino 169 Stack frame 171 Responsabilità del chiamato 170 return value parametri attuali CPU status control link Variabili locali e temporanee frame link 172 ☛ Codice sorgente e stack frame: 0 1 2 3 BP+16 return value BP+12 *p BP+8 i BP BP+4 return address SP BP BP BPold BP-4 j situazione a regime Versione 1.2 © 2000 Marco Torchiano leal -8(%ebp),%eax pushl %eax secondo parametro: *p movl -4(%ebp),%eax pushl %eax primo parametro: i call _set Esempio int set (int i, int* p) { int j; j=*p; *p = i; return j; } Esempio ☛ Il chiamante pone sullo stack i parametri. ☛ Il valore di ritorno viene passato attraverso un registro della CPU per motivi di efficienza. ☛ Per convenzione i parametri vengono depositati in ordine inverso a quello con cui compaiono nella definizione della procedura. ☛ Il record di attivazione viene memorizzato nello stack di esecuzione. ☛ Ad esso vengono affiancate le informazioni di stato del processore. Responsabilità del chiamante Laboratorio di Compilatori a.a 1999/2000 situazione all’ingresso esecuzione della chiamata Esempio pushl %ebp movl %esp,%ebp subl $4,%esp finisce di costruire lo stack frame movl 12(%ebp),%eax movl (%eax),%edx movl %edx,-4(%ebp) j=*p; movl 12(%ebp),%eax movl 8(%ebp),%edx movl %edx,(%eax) *p=i; movl -4(%ebp),%eax return j; leave ret distrugge il frame e restituisce il controllo 43 Politecnico di Torino a.a 1999/2000 Tail recursion 173 175 ☛ Si dice che una procedura è tail-recursive se il suo ultimo statement è la una chiamata ricorsiva alla procedura stessa. ☛ Le funzioni tail-recursive possono essere oggetto di ottimizzazione. 1 int exp(int n) int rexp(int n,int r) { { if(n==1) return 1; if(n==1) return r; return n*exp(n-1); return rexp(n-1,r*n); } L2: movl $1,%eax L3: movl -4(%ebp),%ebx leave ret Versione 1.2 © 2000 Marco Torchiano 5*N+8 if(n==1) return r pushl %ebp movl %esp,%ebp pushl %ebx movl 8(%ebp),%ebx cmpl $1,%ebx je L2 leal -1(%ebx),%eax pushl %eax call _exp imull %ebx,%eax jmp L3 ☛ L’uso dello stack sia per il passaggio di parametri che per memorizzare gli indirizzi di ritorno delle call, consente manipolazioni particolari. ☛ È possibile modificare l’indirizzo di ritorno di una funzione saltando ad altro codice. ☛ È possibile passare dei parametri senza utilizzare l’istruzione di push: ✏ si utilizza un valore di ritorno memorizzato da una call per indirizzare un’area di memoria. 176 return rexp(n-1,r*n) return n*exp(n-1) if(n==1) return 1 14*N+10 Manipolazioni dello stack } Tail recursion 174 Laboratorio di Compilatori pushl %ebp movl %esp,%ebp movl 8(%ebp),%edx movl 12(%ebp),%eax L6: cmpl $1,%edx je L5 imull %edx,%eax decl %edx jmp L6 L5: leave ret invece di: pushl %eax pushl %edx call _rexp Manipolazione dello stack Ret Addr BP void nascosta(){ new_bp printf("Nascosta\n"); } void visibile(){ int new_bp; new_bp = *(&new_bp + 1); *(&new_bp+1) = (int)(&nascosta); BP __asm__(" addl $-4,%ebp"); } BPold Ret Addr &nascosta BPold 44 Politecnico di Torino 177 Laboratorio di Compilatori a.a 1999/2000 Manipolazioni dello stack 179 Codici intermedi ☛ Tipi di codici intermedi ✏ AST, notazione post-fissa, three-address code, RTL void main(){ printf("Eccoci...\n"); __asm__(" jmp my_end my_bg: call _printf add $0x4,%esp jmp my_out my_end: call my_bg .string \"Ciao\\n\" my_out: "); printf("!!!!!!!!\n");} 178 SP Manipolazione dello stack BP ret ✏ ✏ ✏ ✏ Tipologie di quadruple Implementazione Espressioni matematiche e logiche Puntatori, strutture ed array ☛ Trasformazione delle strutture di controllo ✏ if / while / for ✏ flussi di controllo per espressioni logiche ☛ Accesso ad elementi di tipi composti ✏ array ✏ strutture 180 Rappresentazione intermedia ☛ Più semplice da generare rispetto al codice finale. ☛ Più facilmente ottimizzabile. BP prima della call SP ☛ Rappresentazione a quadruple 0x40107f:call 0x401075 <my_bg> 0x401084:”Ciao\n” 0x40108a:add $0xfffffff4,%esp dopo la call Scanner Parser Rappres. Intermedia Ottimizzatore Symbol Table Generatore Codice Assembler Versione 1.2 © 2000 Marco Torchiano 45 Politecnico di Torino 181 Tipi di rappresentazioni ☛ Esistono varie rappresentazioni esaminiamo quattro: ✏ ✏ ✏ ✏ Laboratorio di Compilatori a.a 1999/2000 183 intermedie, Three-address code ☛ È una sequenza di istruzioni del tipo x = y op z dove x , y e z sono: ne ✏ variabili, ✏ costanti, ✏ varibili temporanee generate dal compilatore. syntax tree notazione post-fissa three-address code RTL t1 = - c t2 = b * t1 t3 = - c t4 = b * t3 t5 = t2 + t4 a = t5 ☛ Per confrontarle consideriamo le traduzioni della seguente espressione: a = b * (- c) + b * (- c); 182 Notazione post-fissa e syntax tree 184 ☛ La notazione post-fissa rappresenta così: a b c uminus * b c uminus * + assign ☛ Un sintax tree può essere completo o in forma di dag se vengono identificate le espressioni comuni: assign a b a * * uminus b uminus c Versione 1.2 © 2000 Marco Torchiano c ☛ Assegnamento: ✏ ✏ ✏ ✏ con operatori binari: con operatori unari: di copia: indicizzato x = y op z x = op y x = y x = y[i] x[i] = y ☛ Salto: assign + Tipologie di 3-address code ✏ non condizionale: ✏ condizionale: + * b uminus goto L if x relop y goto L if x goto L ☛ Puntatori ed indirizzi: ✏ puntatore: ✏ indirizzo: x = *y x = &y c 46 Politecnico di Torino 185 RTL 187 ☛ È una rappresentazione più vicina alla macchina. ☛ Utilizza degli pseudo registri e fa riferimento ad una disposizione in memoria delle variabili (GCC). set set set set set set reg_22 mem(reg_18 - 8) reg_23 mem(reg_18 - 8) reg_21 reg_22 + reg_23 reg_24 - mem(reg_18 - 12) reg_25 reg_21 * reg_24 mem(reg_18 - 4) reg_25 a 186 Laboratorio di Compilatori a.a 1999/2000 ☛ Aritmetiche: ✏ su interi: ADDI opnd1, opnd2, res SUBI opnd1, opnd2, res MULI opnd1, opnd2, res DIVI opnd1, opnd2, res NEGI opnd, - , res ITOF opnd, - , res ✏ su floating point: ADDF, SUBF, MULF, DIVF, NEGF, FTOI b c ☛ Logiche: ✏ booleane: AND, OR, NOT, XOR ✏ relazionali: EQ, NE, GT, LT, GE, LE a = ( b + b )*( -c ) Implementazione di 3-address code ☛ Quadruple: rappresentano gli operandi, il tipo di operazione ed il risultato in una struttura a quattro campi: 1 UMINUS c t1 2 Tipi di quadruple ADD b t1 t2 ☛ Triple: non rappresentano esplicitamente il risultato ma identificano esse stesse un risultato: 1 UMINUS c 2 ADD b Versione 1.2 © 2000 Marco Torchiano 188 Tipi di quadruple ☛ Flusso di controllo: ✏ label: LABEL n, -, ✏ salti: JUMP -, -, n JUMP_IF_TRUE opnd, - , n JUMP_IF_FALSE opnd, - , n ☛ Assegnamento: ✏ ASSIGN opnd, size, res ✏ ASSIGNP opnd, size, ptr(res) ✏ ASSIGN[] opnd, offset, res ☛ Su variabili: (1) ✏ ADDRESS opnd, - , res ✏ DEREF opnd, size, res 47 Politecnico di Torino 189 Struttura dati 191 ☛ Le quadruple possono essere realizzate tramite liste linkate, descritte da una struct C: typedef union { t_label label; t_var var; t_offset add; double fval; long ival; } t_op_data; 190 Programma di esempio int i,j,k,l,m; { i = 3 + m*2 - l; typedef struct op_tag { t_code tag; t_op_data data; } t_opnd, * t_op; ☛ Si possono definire un insieme di primitive per operare sulle quadruple: *mkquad(t_code,t_op,t_op,t_op); *append(quad*,quad*); mklabel(void); mk_op(int); mk_var(st_entry*); new_add(t_offset); new_ival(long); new_fval(double); ☛ Occorre definire delle costanti che corrispondano ai vari tipi di quadruple. Versione 1.2 © 2000 Marco Torchiano if(i==1) m=1; else m=2; while(i<5){ i = i+1; } for(i=0; i<10; i=i+1){ m = k+i; } typedef struct q_tag { t_code code; t_op op1,op2,op3; struct q_tag *next; } quad; Primitive quad quad t_op t_op t_op t_op t_op t_op Laboratorio di Compilatori a.a 1999/2000 if( i==j && k==l && m==1 || j>3) { i=1; } } 192 Attributi ☛ Code: contiene le quadruple associate al simbolo non-terminale. ☛ Place: un riferimento alla variabile che contiene il risultato del non-terminale; è utilizzato se si tratta di espressioni. ☛ True, False: nomi delle label a cui salta un’espressione logica. ☛ Nella valutazioni di espressioni vengono generate nuove variabili; esse vengono inserite normalmente nella symbol table del contesto corrente. 48 Politecnico di Torino Espressioni aritmetiche 193 ADD VAR 195 VAR %1 quad Expr place t_opnd ID IVAL t_opnd place Expr Expr 194 1 place code code ID ‘+’ ii ++ NUM 11 Espressioni aritmetiche Expr : Expr '+' Expr { $$.type=compose($1.type,$3.type); $$.place=new_temp($$.type); $1.code $$.code=append( $3.code append($1.code,$3.code) , ADD mkquad(qADDI,$1.place,$3.place,$$.place)); } | Expr '-' Expr { … } … | primary { $$=$1; } ; Versione 1.2 © 2000 Marco Torchiano Espressioni aritmetiche primary: '(' Expr ')' { $$=$2; } | NUM { $$.type=st_lookup("int")->type; $$.code=NULL; $$.place=new_ival($1);} | ID { st_entry* ste; ste=st_lookup($1); $$.code=NULL; $$.type=ste->type; $$.place=mk_var(ste); } t_opnd code Laboratorio di Compilatori a.a 1999/2000 196 Esempio di espressione aritmetica ☛ Ogni nodo genera una quadrupla che usa i risultati dei suoi sotto-nodi. 1 2 3 4 MULI ADDI SUBI ASSIGNV m 3 %1 %2 2 %0 l %0 %1 %2 i 4 3 2 1 ii == 33 ++ mm ** 22 -- ll ;; 49 Politecnico di Torino 197 Costrutti di controllo If-then $3.code : $3.true $5 : $3.false if_stmt : IF '(' bool_exp ')' Stmt { $$=append( append($3.code, mkquad(qLABEL,NULL,NULL,$3.true) ), append($5, mkquad(qLABEL,NULL,NULL,$3.false) ) ); } Versione 1.2 © 2000 Marco Torchiano If-then-else 199 ☛ I blocchi di istruzioni che rappresentano costrutti di controllo ed espressioni booleane richiedono due ulteriori attributi: ☛ True: etichetta a cui il costrutto salta quando la condizione è vera. ☛ False: etichetta a cui il costrutto salta quando la condizione è falsa. ☛ Le etichette vengono generate durante la traduzione delle espressioni booleane o di confronto e vengono utilizzate per guidare i costrutti di controllo. 198 Laboratorio di Compilatori a.a 1999/2000 $3.code :$3.true $5 true jump next :$3.false $7 false :next 200 | IF '(' bool_exp ')' Stmt ELSE Stmt { t_op next; next=new_label(); $$=append(append($3.code, mkquad(qLABEL,NULL,NULL,$3.true)), append(append($5,append( mkquad(qJUMP,NULL,NULL,next), mkquad(qLABEL,NULL,NULL,$3.false))), append($7, mkquad(qLABEL,NULL,NULL,next))) );} ; Esempio di if-then-else ☛ I due rami dell’if vengono racchiusi tra salti e label. JEQ JUMP LABEL ASSIGNV JUMP LABEL ASSIGNV LABEL i 1 2 1 L0 L1 L0 m L2 L1 m L2 if(i==1) if(i==1) mm == 1; 1; else else mm == 2; 2; 50 Politecnico di Torino 201 F Espressioni logiche - confronto rel_exp : Expr REL_OP Expr { if(log_lab_flag){ $$.true=$<bexp>0.true; $1.code $$.false=$<bexp>0.false; }else{ $3.code $$.true=new_label(); J($2) $$.true $$.false=new_label(); } jump $$.false $$.code=append(append($1.code,$3.code), append( T mkquad($2,$1.place,$3.place,$$.true), mkquad(qJUMP,NULL,NULL,$$.false))); } 202 Espressioni logiche - op. booleani bool_exp: bool_exp AND { $<bexp>$.true=new_label(); $<bexp>$.false=$1.false; log_lab_flag=1; } bool_exp { $$.code=append($1.code, append( mkquad(qLABEL,NULL,NULL,$1.true), $4.code)); $$.true=$4.true; T $$.false=$1.false; log_lab_flag=0; } | NOT {$<bexp>$=$<bexp>0;} bool_exp { $$.code=$3.code; $$.true=$3.false; $$.false=$3.true; } $1.code $1.true $4.code F $4.code F T Versione 1.2 © 2000 Marco Torchiano Laboratorio di Compilatori a.a 1999/2000 203 Espressioni logiche - op. booleani | bool_exp OR { $<bexp>$.false=new_label(); $<bexp>$.true=$1.true; log_lab_flag=1; } bool_exp { $$.code=append($1.code,append( mkquad(qLABEL,NULL,NULL,$1.false), $4.code)); $$.true=$1.true; $$.false=$4.false; log_lab_flag=0; $1.code $1.false $4.code F T } | rel_exp ; 204 { $$=$1; } Esempio di espressioni logiche JEQ JUMP LABEL JEQ JUMP LABEL JEQ JUMP LABEL JGT JUMP LABEL ASSIGNV LABEL i j k l m 1 j 3 1 L9 L10 L9 L11 L10 L11 L12 L10 L10 L12 L13 L12 i L13 if( if( i==j i==j && && k==l k==l && && m==1 m==1 || || j>3) j>3) {{ i=1; i=1; }} 51 Politecnico di Torino 205 While :begin $3.code :$3.true $5 jump begin :$3.false Laboratorio di Compilatori a.a 1999/2000 207 while_stmt: WHILE '(' bool_exp ')' Stmt { t_op lab; bgin=new_label(); $$=append(append( mkquad(qLABEL,NULL,NULL, bgin),append( $3.code, mkquad(qLABEL,NULL,NULL,$3.true))), append( $5,append( mkquad(qJUMP,NULL,NULL,bgin), mkquad(qLABEL,NULL,NULL,$3.false))) );} For $3.code :begin $5.code :$5.true $9 $7.code jump begin :$5.false 206 Esempio di while 208 ☛ Una label iniziale consente di fare un ciclo che ripete il test ed esegue il corpo del while. LABEL JLT JUMP LABEL ADDI ASSIGNV JUMP LABEL i i %3 5 1 Versione 1.2 © 2000 Marco Torchiano L5 L3 L4 L3 %3 i L5 L4 while(i<5) while(i<5) {{ ii == i+1; i+1; }} for_stmt: FOR '('Expr';'bool_exp';'Expr')' Stmt { t_op lab; bgin=new_label(); $$=append(append(append( $3.code, mkquad(qLABEL,NULL,NULL,bgin)),append( $5.code, mkquad(qLABEL,NULL,NULL,$5.true))), append(append( $9, $7.code),append( mkquad(qJUMP,NULL,NULL,bgin), mkquad(qLABEL,NULL,NULL,$5.false)))); }; Esempio di for ☛ Come nel while, salvo che prima viene messo il codice di inizializzazione. ASSIGNV LABEL JLT JUMP LABEL ADDI ASSIGNV ADDI ASSIGNV JUMP LABEL 0 i 10 k %5 i %4 i 1 i L8 L6 L7 L6 %5 m %4 i L8 L7 for(i=0; for(i=0; i<10; i<10; i=i+1) i=i+1) {{ m=k+i; m=k+i; }} 52 Politecnico di Torino 209 Accesso con spiazzamento Laboratorio di Compilatori a.a 1999/2000 211 ☛ Per accedere agli elementi di un array o ai campi di una variabile strutturata è necessario associare un nuovo attributo: ☛ Offset: rappresenta lo spiazzamento dell’elemento a cui si vuole accedere, rispetto alla variabile di riferimento, memorizzata nell’attributo place. ☛ È necessario sommare gli offset man mano che vengono calcolati. Accesso ad elementi di array int int int int …… ii == ☛ Costruzione dell’offset: place=v offset=j*8+4*k place=v offset=j*8 place=v offset=null Expr v[j][k]; v[j][k]; array, 5 size=40 array, 2 Expr Expr Expr i,j,k; i,j,k; v[5][2]; v[5][2]; size=8 Expr int size=4 vv [[ jj ][ ][ kk ]; ]; 210 Accesso ad elementi di array ☛ La disposizione in memoria degli array può seguire due strategie: ✏ row major: riga per riga, utilizzata da C e Pascal ✏ column major: colonna per colonna, adottata in Fortran. ☛ Il tipo di un array deve essere costruito da destra verso sinistra; può servire la ricorsione destra: Decl Vdecl Ary : : : | T Vdecl | Decl ‘,’ Vdecl ; ID {$$=$0;} Ary { ... } ; /* empty */ { $$=$<type>0; } '[' NUM ']' { $$=$<type>0; } Ary { $$=te_make_array($2,$5); } ; Versione 1.2 © 2000 Marco Torchiano 212 Accesso ad elementi di array | Expr '[' Expr ']' { t_op ofs; $$.type=$1.type->left; $$.code=append($1.code,$3.code); ofs=new_ival($1.type->left->size); $$.offset=new_temp(st_lookup("int")->type); $$.code=append($$.code, mkquad(qMULI,$3.place,ofs,$$.offset)); if($1.offset!=NULL){ ofs=new_temp(st_lookup("int")->type); append($$.code, mkquad(qADDI,$1.offset,$$.offset,ofs)); $$.offset=ofs; } } 53 Politecnico di Torino 213 Accesso ad elementi di array 215 ☛ Istruzioni di costruzioni dell’offset seguite da un assegnamento con dereferenziazione ed offset. MULI MULI ADDI DRF_OFS j k %0 v 20 4 %1 %2 %0 %1 %2 i int int int int …… ii == ☛ La quadrupla: DRF_OFS base, offset, dest equivale a: dest = base [ offset ] 214 v[j][k]; v[j][k]; 216 ☛ Rappresentazione di un tipo struttura: struct size=12 struct size=8 i = s.cf.b; i offset=0 cf offset=4 a offset=0 b offset=4 Accesso ad elementi di strutture ☛ Costruzione dell’offset ed assegnamento con dereferenziazione ed offset, come per gli array. ☛ Invece degli indici utilizza gli struct struct tag tag {{ offset dei campi all’interno int delle strutture. int i; i; ADDI 4 DRF_OFS s int size=4 Versione 1.2 © 2000 Marco Torchiano Accesso ad elementi di strutture | Expr '.' ID { st_entry* stp; t_op ofs; stp=(st_entry*)ht_lookup($1.type->r.table,$3); $$.type=stp->type; $$.code=$1.code; if($1.offset==NULL){ $$.offset=new_ival(stp->offset); }else{ t_op ofs; ofs=new_ival(stp->offset); $$.offset=new_temp(st_lookup("int")->type); $$.code=append($1.code, mkquad(qADDI,$1.offset,ofs,$$.offset)); } } i,j,k; i,j,k; v[5][2]; v[5][2]; Accesso ad elementi di strutture struct tag { int i; struct x { int a; int b; } cf; } s; Laboratorio di Compilatori a.a 1999/2000 4 %0 %0 i struct struct xx {{ int int a; a; int int b; b; }} cf; cf; }} s; s; ... ... ii == s.cf.b; s.cf.b; 54 Politecnico di Torino 217 Laboratorio di Compilatori a.a 1999/2000 Uso avanzato di Bison ☛ Gestione degli errori sintattici ✏ errori negli statement ✏ gestione del contesto ✏ interazione con lo scanner ☛ Debugging dei parser 219 Statement ed espressioni stmt : … | error ';' { error("syntax error in statement"); } compound : … | '{' stmts error '}'{ error("missing ; before '}'");} exp: … |'(' error ')'{ error("syntax error in expression"); } ✏ conflitti shift-reduce ✏ conflitti reduce-reduce yyerror(char* s){ } void error(char* s){ printf("%s:%d: %s\n",filename,yylineno,s); } 218 file Grammatica : ; funcs : | ; func : funcs /* empty */ funcs func ID '(' ')' compound ; stmts : /* empty */ | stmts stmt ; Versione 1.2 © 2000 Marco Torchiano 220 stmt : exp ';' | compound ; compound: '{' stmts '}' ; exp : NUM | exp '+' exp | exp '-' exp | exp '*' exp | exp '/' exp | '-' exp %prec NEG ; Errori negli statement NUM stmts -> stmts . stmt 10 compound -> '{' stmts . '}' compound -> '{' stmts . error '}' error exp -> NUM . 12 exp stmt -> exp . ';' 18 exp -> exp . '+' exp 11 stmt -> error . ';' compound -> '{' stmts error . '}' ... ‘+’ + 25 (exp) 10 (stmts) 3 + ; exp -> exp '+' . exp 25 55 Politecnico di Torino 221 Contesto func : ID '(' ')' compound 223 { new_context($1); } { out_context(); } Interazione con lo scanner 224 funcs : … | funcs error { if(yychar<256){ char buf[255]; sprintf(buf,"syntax error before '%c'",yychar); error(buf); yyclearin; }else error("syntax error"); } exp: … | '(' error ';' { error("missing ')' before ';'"); pushback(';'); } void pushback(char ch){ Versione 1.2 © 2000 Marco Torchiano Esempio 1 2 3 4 5 6 7 8 9 10 11 12 13 14 if(!context_printed){ if(context) printf("%s: In function %s:\n", filename,context); else printf("%s: At top level:\n",filename); context_printed = 1; } 222 Laboratorio di Compilatori a.a 1999/2000 f(){ 1 + 1 ; 1 + ( 6 + ) ; 1 + (5 + ; 3 + ; { 3 + 2 ; } } ? g(){ { 3 + 3 } { 3 + (3-) } 3+1 } prova.c: prova.c:In Infunction functionf:f: prova.c:3: prova.c:3:syntax syntaxerror errorin inexpression expression prova.c:4: missing ')' before prova.c:4: missing ')' before';'';' prova.c:5: prova.c:5:syntax syntaxerror errorin instatement statement prova.c: prova.c:At Attop toplevel: level: prova.c:8: prova.c:8:syntax syntaxerror errorbefore before'?' '?' prova.c: In function g: prova.c: In function g: prova.c:10: prova.c:10:missing missing;;before before'}' '}' prova.c:11: prova.c:11:syntax syntaxerror errorin inexpression expression prova.c:11: prova.c:11:missing missing;;before before'}' '}' prova.c:14: prova.c:14:missing missing;;before before'}' '}' Debugging del parser ☛ L’opzione -v di Bison genera un file, con estensione out, in cui è descritto il parser generato. ☛ La struttura del file è la seguente: ✏ ✏ ✏ ✏ ✏ elenco delle anomalie e dei conflitti (eventualmente risolti) regole della grammatica senza alternative lista dei terminali con i loro usi lista dei non terminali, con definizioni e usi elenco degli stati del parser nello nelloscanner scanner perchè perchèunput unput èèuna unamacro! macro! unput(ch); } 56 Politecnico di Torino 225 Liste ☛ rule 1 ☛ rule 2 227 S -> '{' L '}' … L -> /* empty */ S : ‘{‘ L ‘}’ ‘{‘ M ‘}’ … M : ID '(' ')' { /* */ } '{' '}' | ID '(' ')' '{' '?' '}' ☛ rule 6 Liste L : /* empty */ |E |LE ; Riscrittura L L ε E L Versione 1.2 © 2000 Marco Torchiano L : /* empty */ | Lst ; Lst : E | Lst E ; @1 -> /* empty */ state 12 M -> ID '(' ')' . @1 '{' '}' (rule 7) M -> ID '(' ')' . '{' '?' '}' (rule 8) '{' shift, and go to state 14 '{' [reduce using rule 6 (@1)] $default reduce using rule 6 (@1) @1 go to state 15 228 èè ambigua ambigua Azioni intermedie S : ‘{‘ L ‘}’ … L : /* epsilon */ | E | L E ; E : ID ; state 1 S -> '{' . L '}' … (rule 1) ID shift, and go to state 2 ID [reduce using rule 2 (L)] $default reduce using rule 2 (L) L go to state 3 E go to state 4 226 Laboratorio di Compilatori a.a 1999/2000 Reduce-reduce e contesto ☛ S : … ‘{‘ A ‘}’ ☛A:B | A ',' B ☛ rule 20 C -> ID ☛ rule 21 D -> ID BB CC DD state 27 C -> ID . (rule 20) D -> ID . (rule 21) '}' reduce using rule 20 (C) '}' [reduce using rule 21 (D)] ',' reduce using rule 20 (C) ',' [reduce using rule 21 (D)] $default reduce using rule 20 (C) :: || ;; :: ;; :: || ;; CC DD ID ID ID ID DD ',' ',' ID ID 57 Politecnico di Torino 229 a.a 1999/2000 Laboratorio di Compilatori Reduce-reduce ed epsilon state 16 S -> … '{' N . '}' (rule 1) N -> N . O (rule 10) N -> N . Q (rule 11) '}' shift, and go to state 19 ID reduce using rule 12 (O) ID [reduce using rule 14 (Q)] '}' [reduce using rule 12 (O)] '}' [reduce using rule 14 (Q)] '!' reduce using rule 12 (O) '!' [reduce using rule 14 (Q)] $default reduce using rule 12 (O) … Versione 1.2 © 2000 Marco Torchiano SS :: …… ‘{‘ ‘{‘ NN ‘}’ ‘}’ …… NN :: /* /* empty empty */ */ 9 || NN OO 10 || NN QQ 11 ;; OO :: /* /* empty empty */ */ 12 || OO ID 13 ID ;; QQ :: /* /* empty empty */ */ 14 || QQ '!’ 15 '!’ ;; 58