Laboratorio di Compilatori

Documenti analoghi
Esercitazioni di Linguaggi e Traduttori

Copyright. Esercitazioni di Linguaggi e Traduttori. Contenuti. Analisi lessicale. lex - un generatore di analizzatori lessicali

LEX. Espressioni regolari in Lex

Riconoscitori e analizzatori sintattici. Scanning e parsing. Funzionamento di un parser: la tecnica Shift/Reduce. Esempio

Yet Another Compiler-Compiler. Generazione automatica di analizzatori sintattici

Definizioni syntax-directed

Linguaggi e Ambienti di Programmazione

Dispensa YACC: generalità

Analizzatori Lessicali con JLex. Giuseppe Morelli

Dispensa YACC. 1.1 YACC: generalità

Elementi lessicali. Lezione 4. La parole chiave. Elementi lessicali. Elementi lessicali e espressioni logiche. Linguaggi di Programmazione I

Corso di Laurea Magistrale in Ingegneria Informatica A.A Linguaggi Formali e Compilatori LEX, FLEX, JLEX. Giacomo PISCITELLI

Analizzatore lessicale o scanner. Lo scanner rappresenta un'interfaccia fra il programma sorgente e l'analizzatore sintattico o parser.

Analizzatore Lessicale Parte I Scanner

1

Analisi lessicale. Scopi Tecniche Strumenti - Lex. Introduzione ai compilatori - UNINA 1

POLITECNICO DI TORINO. Laboratorio di Compilatori Corso di Linguaggi e Traduttori. Esercitazione 1. a.a 2010 / Linguaggi?

Fasi di un Compilatore

Corso di Fondamenti di Informatica Il sistema dei tipi in C++

Verificare se una grammatica e LL(1) e costruirne la tabella di parsing. Verificare se una grammatica e LR(0) e costruirne la tabele ACTION e GOTO

Primi passi con JFlex

Lezione 6 Introduzione al C++ Mauro Piccolo

Fondamenti di Informatica 6. Algoritmi e pseudocodifica

GESTIONE DEI FILE IN C. Docente: Giorgio Giacinto AA 2008/2009

Pumping lemma per i linguaggi Context-free

Costruzione dell insieme dei Follow

Algoritmi, Strutture Dati e Programmi. UD 2.b: Programmazione in Pascal

Linguaggio C: introduzione

I CARATTERI E LE STRINGHE

INTRODUZIONE ALLA PROGRAMMAZIONE AD ALTO LIVELLO IL LINGUAGGIO JAVA. Fondamenti di Informatica - D. Talia - UNICAL 1. Fondamenti di Informatica

Riconoscitori e analizzatori sintattici

Dispensa 2. Data una grammatica context free esistono tre metodi diversi per costruirne la parsing table per un parser LR:

Programma del corso. Elementi di Programmazione. Introduzione agli algoritmi. Rappresentazione delle Informazioni. Architettura del calcolatore

Funzioni, Stack e Visibilità delle Variabili in C

Le basi del linguaggio Java

Esercitazione 11. Liste semplici

Costanti e Variabili

Unità F1. Obiettivi. Il linguaggio C. Il linguaggio C++ Linguaggio C. Pseudolinguaggio. Primi programmi

ESECUZIONE DI PROGRAMMI C SU MACCHINE REALI. Docente: Giorgio Giacinto AA 2008/2009. formalizzazione degli algoritmi in linguaggio C

L utility Unix awk [Aho-Weinberger-Kernighan]

Linguaggio C - le strutture di controllo: sequenza, selezione, iterazione

Le direttive del Preprocessore

Argomenti Avanzati.! I puntatori! Stack! Visibilità delle Variabili

Fondamenti d Informatica: linguaggi formali. Barbara Re, Phd

Caratteristiche di un linguaggio ad alto livello

Generatori di analizzatori lessicali e sintattici

Espressione di chiamata di funzione

Traduzione guidata dalla sintassi

File binari e file di testo

Caratteri e stringhe

Unità Didattica 1 Linguaggio C. Fondamenti. Struttura di un programma.

Esercizi su strutture dati

Cognome e Nome : Corso e Anno di Immatricolazione: Modalità di Laboratorio (Progetto/Prova) :

L intero è o il valore zero o una stringa di cifre che inizia con una cifra diversa sa zero.

Primi passi col linguaggio C

Corso di Laboratorio di Sistemi Operativi

Un esempio per iniziare. Il controllo del programma in C. Altri cenni su printf() Esercizi (printf) printf( 8!=%d, fatt);

Scope delle variabili e passaggio parametri. Danilo Ardagna Politecnico di Milano

Esercizio 2 (punti 7) Dato il seguente programma C: #include <stdio.h> int swap(int * nome, int length);

Verso i puntatori: Cosa è una variabile?

Input/Output. Lettura e scrittura Caratteri e Stringhe: Terminale e file. Input/output. caratteri stringhe formattato ascii binari

Intro. Traduzione guidata dalla sintassi. Attributi. Due notazioni a diversi livelli. Due notazioni a diversi livelli. Il flusso concettuale

Variabili. Unità 2. Domenico Daniele Bloisi. Corso di Fondamenti di Informatica Ingegneria delle Comunicazioni BCOR Ingegneria Elettronica BELR

Introduzione alla programmazione

Input/Output di numeri

ADT LISTA: altre operazioni non primitive ADT LISTA COSTRUZIONE ADT LISTA COSTRUZIONE ADT LISTA (2)

Input/output da file I/O ANSI e I/O UNIX FLUSSI E FILE FLUSSI FLUSSI di TESTO FLUSSI BINARI FILE

Espressioni aritmetiche

Funzioni in C. Funzioni. Strategie di programmazione. Funzioni in C. Come riusare il codice? (2/3) Come riusare il codice? (1/3)

Array. Corso di Laurea Ingegneria Informatica Fondamenti di Informatica 1. Dispensa 11. A. Miola Dicembre 2007

Corso di Fondamenti di Informatica Linguaggi di Programmazione

Lezione 8 Struct e qsort

Variabili. Unità 2. Domenico Daniele Bloisi. Corso di Programmazione e Metodi Numerici Ingegneria Aerospaziale BAER

Perché il linguaggio C?

Informatica ALGORITMI E LINGUAGGI DI PROGRAMMAZIONE. Francesco Tura. F. Tura

Linguaggio C Struttura dei programmi

Foglio Elettronico Lezione 1

Linguaggio C - sezione dichiarativa: costanti e variabili

Introduzione a Matlab

Array in Fortran 90. Ing. Luca De Santis. Anno accademico 2006/2007. DIS - Dipartimento di informatica e sistemistica

Funzioni e. Alessandra Giordani Mercoledì 16 maggio 2012

Le strutture. Una struttura C è una collezione di variabili di uno o più tipi, raggruppate sotto un nome comune.

Variabili e Istruzioni

Gestione dei file. Stefano Ferrari. Università degli Studi di Milano Programmazione. anno accademico

Descrizione delle operazioni di calcolo. Espressioni costanti semplici

Analisi Lessicale. File

Il preprocessore. Direttiva define Direttiva include Direttiva if

Strategie di programmazione

La sintassi del C APPENDICE H

Le basi del linguaggio Java

Linguaggio C: le funzioni. Visibilità variabili e passaggio parametri

5. Quinta esercitazione autoguidata: liste semplici

LINGUAGGI DI ALTO LIVELLO. Si basano su una macchina virtuale le cui mosse non sono quelle della macchina hardware

Linguaggi di Programmazione

IL PRIMO PROGRAMMA IN C

Grammatiche. Grammatiche libere da contesto Grammatiche regolari Potenza delle grammatiche libere e regolari Struttura di frase: Alberi di derivazione

6 - Blocchi e cicli. Programmazione e analisi di dati Modulo A: Programmazione in Java. Paolo Milazzo

LINGUAGGI DI ALTO LIVELLO

Valutazione di espressioni

MATLAB I/O. Informatica B - A.A. 2012/2013 ACQUISIZIONE DI INPUT DA TASTIERA

Transcript:

1 3 Analisi lessicale Laboratorio di Compilatori 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: torchiano@polito.it Espressioni regolari Lex 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 2 Contenuti 4 Espressioni regolari Analisi lessicale Analisi sintattica Regole semantiche Controllo dei tipi Ambienti di esecuzione Codici intermedi Tecniche per il parsing Costituiscono un metodo semplice ed efficace per descrivere insiemi di stringhe di caratteri. Opportuni operatori consentono di indicare caratteri classi di caratteri opzionalità exp? ripetizione (0 o più volte) exp * ripetizione (1 o più volte) exp + c o anche c [a,b,c] o [a-c] alternativa exp1 exp2 concatenazione exp1 exp2 ambito di operatori ( exp ) Versione 1.2 2000 Marco Torchiano 1

5 Esempi di espressioni regolari numero intero positivo [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]+) ) 7 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. gli apici consentono di distinguere un carattere in ingresso ( + ) da un operatore (+). Espressioni regolari Lex Programma C 6 Esercizi 8 Espressioni regolari in Lex 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. 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. Versione 1.2 2000 Marco Torchiano 2

9 Espressioni regolari in Lex 11 Espressioni regolari in Lex...continua......continua... I caratteri non alfabetici possono essere anche descritti facendoli precedere dal carattere \ l espressione xyz\+\+ rappresenta la sequenza x y z + + nel testo di ingresso. Le classi di caratteri vengono descritte mediante gli operatori []: l espressione [0123456789] rappresenta una cifra nel testo di ingresso. Nel descrivere classi di caratteri, il segno - indica una gamma di caratteri: l espressione [0-9] rappresenta una cifra nel testo di ingresso. Il carattere di fine riga viene descritto dal simbolo \n. Il carattere di tabulazione viene descritto dal simbolo \t. L operatore? indica l espressione precedente è opzionale: ab?c indica sia la sequenza ac che abc. L operatore * indica l espressione precedente può essere ripetuta 0 o più volte: ab*c indica tutte le sequenze che iniziano per a, terminano per c e hanno all interno un numero qualsiasi di b. 10 Espressioni regolari in Lex 12 Espressioni regolari in Lex...continua......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.. 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 indica un alternativa tra due espressioni: 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. Versione 1.2 2000 Marco Torchiano 3

13 Azioni associate alle espressioni regolari continua... 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. 15 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. 14 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. 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 ) Versione 1.2 2000 Marco Torchiano 4

17 Definizioni...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 %. Tutte le righe presenti nella sezione delle procedure (la terza) del programma sorgente sono ricopiate nel file C generato da Lex. Esistono due tipi di ambiguità lessicali: 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. 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. 18 Esercizi 20 Esempio 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. 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. Versione 1.2 2000 Marco Torchiano 5

21 Risoluzione delle ambiguità lessicali 23 Dipendenza dal contesto 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 cerca di riconoscere il secondo apice il più lontano possibile: cosi, dato il seguente ingresso first quoted string here, second here riconoscerà 36 caratteri invece di 7. Una regola migliore è la seguente: [^ \n]+ {return QUOTED_STRING 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. 22 Esercizi 24 Contesto di inizio e fine riga 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. 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. Versione 1.2 2000 Marco Torchiano 6

25 Dipendenza dal contesto destro 27 Uso di variabili flag 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. 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. 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 alla seconda miglior regola che riconosce lo stesso input, oppure ad una regola che riconosca un prefisso dello stesso input. 26 Dipendenza dal contesto sinistro 28 Esempio È 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. 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 Versione 1.2 2000 Marco Torchiano 7

29 Uso di condizioni iniziali sulle regole (stati inclusivi) 31 Esempio Le regole la cui espressione regolare inizia con <state> 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) Il seguente programma gestisce pseudo-commenti del tipo // $var+ %s comment %% <comment>\$[a-za-z]+[-+] {process(yytext) // {BEGIN(comment) \n {BEGIN(INITIAL) /* ignore blanks*/ \t /* and tabs */... other rules 30 Uso di condizioni iniziali sulle regole (stati inclusivi) continua 32 Uso congiunto di più analizzatori lessicali (stati esclusivi) 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. È 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. Versione 1.2 2000 Marco Torchiano 8

33 Eliminazione dei commenti 35 Esercizi Un possibile approccio all eliminazione dei commenti del linguaggio C: %% "/*" { 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 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. 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 %% \n ++line_num "/*" BEGIN(comment) <comment>[^*\n]* <comment>"*"+[^*/\n]* <comment>\n ++line_num <comment>"*"+"/" BEGIN(INITIAL)... altre regole 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. Versione 1.2 2000 Marco Torchiano 9

37 Ingresso e uscita 39 Il file prodotto continua Per default, i file yyin e yyout sono inizializzati rispettivamente a stdin e stdout. Il programmatore può alterare questa situazione reinizializzando tali variabili globali. 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: 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. 38 Il file generato 40 Il file prodotto continua yylex() 1 2 inizializzazione del file yyin (default=stdin) scansione del file di ingresso azione esegue return elaborazione del simbolo trovato EOF yywrap() altri file? 3 si reinizializzazione del file yyin no fine dell analisi Per far avvenire la lettura un carattere alla volta si può utilizzare il codice seguente: %{ #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(). Versione 1.2 2000 Marco Torchiano 10

41 Il file prodotto continua 43 Inclusione di file 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. 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. 42 Regole di fine file 44 Inclusione di file continua 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: \ { BEGIN(quote)... <quote><<eof>> { error( EOF in string ) 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) Versione 1.2 2000 Marco Torchiano 11

45 Inclusione di file continua 47 Inclusione di file continua Esempio di analizzatore che elabora file inclusi con annidamento. %x incl %{ #define MAX_INCLUDE_DEPTH 10 YY_BUFFER_STATE include_stack[max_include_depth] int include_stack_ptr = 0 % %% include [a-z]+ [^a-z\n]*\n? BEGIN(incl) ECHO ECHO %% int yywrap() { if ( --include_stack_ptr < 0 ) return(1) else { yy_switch_to_buffer( include_stack[include_stack_ptr]) return(0) 46 Inclusione di file continua 48 Uso della funzione yylex() <incl>[ \t]* /* 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) 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. Versione 1.2 2000 Marco Torchiano 12

49 Lex: parametri ed opzioni 51 Grammatiche context free continua... Lex viene invocato con la seguente sintassi: lex [opzioni] file.lex Si ottiene come risultato il file lex.yy.c. Tra le opzioni, le più comuni sono: -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. Il concetto di ricorsione è fondamentale nella scrittura di grammatiche context-free. È lo strumento per descrivere delle sequenze lunghe a piacere. Lista (grammatica ricorsiva sinistra) : List List, Element List =Element Lista (grammatica ricorsiva destra) : List Element Tail Tail == ==, Element Tail Tail == ==ε 50 Linguaggi CF e Riconoscitori 52 Alberi di derivazione 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 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 n 1,..., n m, se e solo se la grammatica contiene la regola n n 1... n m Versione 1.2 2000 Marco Torchiano 13

53 Alberi di derivazione 55 Ambiguità List List, el List =el List List List el, el, el List el Tail Tail == ==, el Tail Tail == ==ε List Tail Tail el, el, el Tail ε 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 if (i=1) then if (j=2) then a:=0 else a:=1 data la grammatica: S I I if C then I I if C then I else I I =var := E C ( E = E ) E algebric expression 54 Costrutti Pascal-like 56 Espressioni algebriche 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 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 Versione 1.2 2000 Marco Torchiano 14

57 Esempio 59 Separatori e terminatori 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: I =I 1 I 1 if C then I 1 I 1 if C then I 2 else I 1 I 1 =I 3 I 2 if C then I 2 else I 2 I 2 =I 3 I 3 =var := E Una lista, eventualmente vuota, di identificatori separati da List id_list List ε id_list id_list id id_list id Una lista, eventualmente vuota, di identificatori terminati da List List id List ε List List id List id List ε 58 Esercizi 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: if C then if C then A end else A end 60 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). Versione 1.2 2000 Marco Torchiano 15

61 Riconoscitori Bottom-Up 63 La tecnica shift-reduce continua 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. Per un corretto funzionamento bisogna: 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. 62 La tecnica shift-reduce continua 64 LALR(1) 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. S. exp 0 2 exp S exp. exp. exp + T exp exp. + T + exp. T exp. NUM exp exp +. T 4 NUM NUM T T NUM. 1 exp exp + T. 5 T S exp T. 3 :: exp exp :: exp '+' '+' T T T :: NUM Versione 1.2 2000 Marco Torchiano 16

65 LALR(1) 67 LALR(1) 2 + 4 T 5 exp NUM T 0 NUM 1 3 0 NUM + NUM shift, go to state 1 eof 2 + 4 T 5 exp NUM T 0 NUM 1 3 3 0 NUM + NUM reduce ( exp T ) 0 exp eof 1 0 NUM + NUM eof exp T NUM 2 0 NUM + NUM eof 66 LALR(1) 68 LALR(1) 2 + 4 T 5 exp NUM T 0 NUM 1 3 1 0 NUM + NUM reduce ( T NUM ) 0 T eof 2 + 4 T 5 exp NUM T 0 NUM 1 3 2 0 NUM + NUM shift, go to state 4 eof T NUM 3 0 NUM + NUM eof exp T NUM 4 2 0 NUM + NUM eof Versione 1.2 2000 Marco Torchiano 17

69 LALR(1) exp T 2 0 3 + NUM NUM 4 1 T 5 exp T NUM 4 2 0 1 4 2 0 NUM + NUM shift, go to state 1 NUM + NUM eof eof 71 LALR(1) 2 exp 0 T 3 + NUM NUM 4 1 T 5 exp exp T T NUM + NUM 5 4 2 0 NUM + NUM reduce ( exp exp + + T ) 2 0 0 exp NUM + NUM eof eof 70 LALR(1) 2 exp 0 T 3 + NUM NUM 4 1 T 5 exp T T NUM NUM 1 4 2 0 5 4 2 0 NUM + NUM reduce ( T NUM ) 4 2 T 0 NUM + NUM eof eof 72 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. Versione 1.2 2000 Marco Torchiano 18

73 Il formato del file di ingresso 75 Dichiarazioni continua Il file di ingresso su cui opera Yacc è formato da tre sezioni: 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 */. 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. 74 Dichiarazioni 76 Codifica della grammatica 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). I simboli terminali sono nomi o letterali: un letterale è un singolo carattere racchiuso tra apici. 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. Versione 1.2 2000 Marco Torchiano 19

77 Esempio 79 Formato del programma generato %token integer %start Expression %% Expression Term Factor : Expression + Term Term : Term * Factor Factor : integer ( 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 *) che viene invocata quando si incontra un errore. Inoltre, il programma generato non contiene il main(), che deve essere definito dal programmatore. 78 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. 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. Versione 1.2 2000 Marco Torchiano 20

81 Integrazione con l analizzatore lessicale continua 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. 83 Esempio: inclusione del sorgente language.y #include lex.yy.c language.l Lex Lex Yacc lex.yy.c y.tab.c #include lex.yy.c include cc cc parser.obj 82 Integrazione con l analizzatore lessicale continua 84 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. 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. Versione 1.2 2000 Marco Torchiano 21

85 Esempio: compilazione separata 87 Conflitti shift-reduce language.y Yacc #include y.tab.h language.l -d -d y.tab.h Lex Lex y.tab.c include cc cc cc cc scanner.obj #include y.tab.h lex.yy.c parser.obj S then E if then E if Top of Stack 1) S ==if E then S 2) S ==if E then S else S 3) S ==var = E Il prossimo simbolo in ingresso è else. Possono succedere due cose: ridurre le prime quattro voci dello stack, secondo la produzione 1 introdurre else nello stack secondo quanto previsto dalla produzione 2. In queste situazioni, Yacc decide, in mancanza di altri suggerimenti, di eseguire lo shift. 86 Ambiguità e conflitti in Yacc 88 Conflitti reduce-reduce 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. 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). Versione 1.2 2000 Marco Torchiano 22

89 Definizione degli operatori e gestione delle priorità 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) E == ==E + E 2) E == ==E * E 3) E == == ( E ) 4) E == ==integer Questa grammatica è fortemente ambigua. 91 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. 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à. 92 Esempio %token integer %left + - /* lowest priority */ %left * / %left uminus /* highest priority */ %% E : E + E E - E E * E E / E - E %prec uminus ( E ) integer Versione 1.2 2000 Marco Torchiano 23

93 Gestione degli errori sintattici continua... 95 Gestione degli errori sintattici...continua... In genere quando un parser incontra un errore non dovrebbe terminare brutalmente l esecuzione 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 Per default, un parser generato da YACC, quando incontra un errore: segnala, tramite yyerror(), un parse error restituisce il valore 1 Il comportamento di default può essere accettabile per un semplice parser interattivo. 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. 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 94 Gestione degli errori sintattici...continua... 96 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 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 ) Versione 1.2 2000 Marco Torchiano 24

97 Gestione degli errori sintattici...continua 99 Definizioni Guidate dalla Sintassi 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. 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. 98 Regole semantiche 100 Attributi sintetizzati 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 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 E 1 + T E.value = E 1.value + T.value E T E.value = T.value T number T.value = number.value Versione 1.2 2000 Marco Torchiano 25

101 Attributi ereditati 103 La semantica in Yacc continua... Sono utili per esprimere la dipendenza di costrutti di un linguaggio dal contesto in cui si trovano. D L : T L.type = T.type L L 1, id L 1.type = L.type new_var(id.name,l.type) L id new_var(id.name,l.type) T integer T.type = type_int 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. 102 Attributi L 104 La semantica in Yacc...continua 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). 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 Versione 1.2 2000 Marco Torchiano 26

105 Esempio 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 Un azione può contenere istruzioni qualsiasi, anche facenti riferimento a variabili globali: A : B C D { printf( %d\n,i) 107 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. %{ 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... */ 106 Tipi dei valori semantici 108 Calcolo di attributi sintetizzati 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. 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 Versione 1.2 2000 Marco Torchiano 27

109 Sintesi di attributi in Lex Se ad un simbolo terminale sono associati degli attributi, bisogna definirne il tipo nella sezione delle dichiarazioni con la sintassi seguente: %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) 111 Calcolo di attributi ereditati continua... 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 ε. 110 Esercizi 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. 112 Calcolo di attributi ereditati continua... Una produzione che usa una copy-rule: (1) Decl T D1 D1.i = T.s (2) D1 id var(d1.i,id.n) Situazione dello stack prima di ridurre D1 id.n T.s... $1 contesto della regola (2) Versione 1.2 2000 Marco Torchiano 28

113 T type: type: int int Calcolo di attributi ereditati Decl Decl1 type: type: int int id name: name: jj...continua... 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. 115 Calcolo di attributi ereditati T type: type: int int id name: name: ii Decl, Decl1 type: type: int int Empty type: type: int int Decl1 type: type: int int id name: name: jj...continua... 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 114 Calcolo di attributi ereditati 116 Calcolo di attributi ereditati...continua......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) 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>$. Versione 1.2 2000 Marco Torchiano 29

117 Esempio %{ 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 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 %% 119 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 L b a L M L b a M ε 118 Trasformazione della grammatica LL È possibile evitare l uso degli attributi ereditati trasformando la grammatica. D T L LL L : T integer real L, id id DD i, j : integer TT D L T DD LL id L, id L : T integer real LL i, j : integer TT 120 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. Versione 1.2 2000 Marco Torchiano 30

121 Abstract Syntax Tree 123 Controllo dei tipi 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 if B then S 1 else S 2 if-then-else Type expressions sistemi di tipi, espressioni di tipi, costruttori di tipo. Costruzione di type-expression Symbol tables Implementazione di un type-checker strutture dati grammatica semantica B S 1 S 2 122 Esercizi 124 Sistemi di tipi 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. 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. Versione 1.2 2000 Marco Torchiano 31

125 Type-expressions 127 Costruttori di tipo 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: void : denota l assenza di un tipo, type_error : indica un errore rilevato durante il controllo dei tipi. È possibile assegnare un nome ad un tipo o ad un suo campo. Un nome è una type-expression. Una funzione mappa un elemento del proprio dominio in un elemento del range. Funzioni: T 1 T 2 T 1 : tipo del dominio, T 2 : tipo del range. La funzione int* f(char a, char b) viene rappresentata dalla seguente type expression: (char x char) pointer(int) Teoricamente una funzione può avere come dominio e codominio tipi qualsiasi: ((int int) x (int int)) (int int) 126 Costruttori di tipo 128 Grafo dei tipi Array: array( I, T ) I : dimensione dell array T : type expression Puntatori: pointer( T ) Prodotto: T 1 X T 2 Strutture: struct( T ) Un modo efficace di rappresentare le type expression è l uso di grafi (alberi o DAG). (char x char) pointer(int) char v[10] array(10,char) struct { int i char s[5] struct((i x int )x( s x array(5,char))) X char pointer integer Versione 1.2 2000 Marco Torchiano 32