Generatori di analizzatori sintattici Generatori di parser Data una specifica sintassi (come uno grammatica context-free), il parser si occupa di leggere i token e li raggruppa in strutture linguistiche. I Parser vengono in genere creati da una CFG utilizzando un generatore di parser (come Yacc, Bison o Java CUP). Il parser verifica la correttezza sintassi e può produrre un messaggio di errore. Quando viene ricosciuta una struttura sintattica il parser di solito costruisce un albero di sintattico (AST) che è una rappresentazione concisa della struttura del programma, che guida la trasformazione semantica. 1 Yacc: Introduzione Yacc è un tool disponibile su Unix, ma non solo, e permette di generare dei parser. Quando si scrive un programma in Yacc si descrivono le produzioni della grammatica del linguaggio da riconoscere e le azioni da intraprendere per ogni produzione. Yacc gestisce le grammatiche LARL(1). Yacc genera una funzione (parser) per riconoscere l’input. Il parser usa l’analizzatore lessicale per prelevare dall’input i token e riorganizzarli in base alla produzioni della grammatica utilizzata. Quando una produzione viene riconosciuta viene eseguito il codice ad essa associata. Struttura di un Programma Yacc Ogni programma Yacc consta di tre sezioni: dichiarazioni, regole e programmi ed ha il seguente aspetto: Dichiarazioni %% Regole %% Sezione routines ausiliarie La sezione delle regole `e l’unica obbligatoria. I caratteri di spaziatura (blank, tab e newline) vengono ignorati. I commenti sono racchiusi, come in C, tra i simboli /* e */ 2 Sezione regole La sezione regole `e composta da una o pi`u produzioni espresse nella forma: A : BODY ; dove A rappresenta un simbolo non terminale e body rappresenta una sequenza di uno più simboli sia terminali che non terminali. I simboli : e ;sono separatori. Nel caso la grammatica presenti più produzioni per lo stesso simbolo terminale, queste possono essere scritte senza ripetere il non terminale usando il simbolo | Yacc: Sezione Definizioni Nella sezione definizione si definiscono alcune informazioni globali da dover usare per interpretare la grammatica. Tramite l’istruzione: ù %token1 token2 …tokenn si definiscono quali sono i token inseriti nelle regole che sono il risultato dell’analisi lessicale. Tramite l’istruzione: % start assioma si definisce qualè il non terminale della grammatica da considerare come assioma (per default il primo non terminale incontrato). 3 Yacc: Azioni Ad ogni regola può essere associata un’azione che verrà eseguita ogni volta che la regola viene riconosciuta. Le azioni sono istruzioni C e sono raggruppate in un blocco. A : B C D {printf (“ciao”) Le azioni possono apparire ovunque nel body di una regola. Le azioni possono scambiare dei valori con il parser tramite delle pseudo-variabili introdotte dal simbolo ($$, $1, $2, …) A : B {$$ = 1} C {$1 = 2; $2 = 12} La pseudo-variabile $$ è associata al lato sinistro della produzione mentre le pseudo-variabili $n sono associate al non terminale di posizione n nella parte destra della produzione. Yacc: Analisi Lessicale Yacc si avvale si un analizzatore lessicale (yyLex per leggere l’input e convertirlo in token (più eventuali valori) da passare al parser. Un possibile analizzatore lessicale pu`o essere creato usando LeX. I token sono passati al parser sotto forma di interi. Quindi parser ed analizzatore lessicale devono accordarsi su quali valori rappresentano i token. L’accordo viene preso in modo automatico da Yacc definendo i vari token tramite istruzioni C #define L’analizzatore lessicale pu`o associare ai token un valore assegnandolo alla variabile predefinita yylval 4 Yacc: Parser Il parser generato da Yacc `e un automa a stati finiti di tipo push-down in grado di avere un token di lookahead. L’automa ha solo 4 azioni: shift, reduce, accept ed error. In base allo stato corrente (simbolo sul top dello stack) il parser decide se necessita di un token di lookahead (ottenibile usando yylex per decidere che azione intraprendere. Usando lo stato corrente ed il token di lookahead decide quale azione intraprende e la espleta. Azione di shift: usa sempre un token di lookahead, e consiste nel confrontare tale token con il token corrente ed in caso di match spilare lo stato dallo stack, inserire il nuovo stato e saltare il token di lookahead. Azione di reduce: evitano il crescere incontrollato dello stack, e sono usate quando il parser esamina il lato destro di una produzione e lo sostituisce con il lato sinistro della stessa. Può servire un token di lookahead. Azione di accept: l’input appartiene al linguaggio descritto dalla grammatica. Azione di error: l’input si `e rilevato non appartenente al linguaggio descritto dalla grammatica. Yacc: Ambiguità e Conflitti Le produzioni di una grammatica possono essere ambigue, come ad esempio: E:E+E infatti se in input si ha la stringa E + E + E possibile interpretarla sia come E + (E +E) che come (E +E) +E. Yacc è in grado di accorgersi di tale ambiguità. Il parser può applicare un’azione di reduce alla parte di stringa E + E ottenendo E E e quindi riapplicare tale azione; oppure può applicare un’azione di shift E + E e quando ha letto tutta la stringa applicare le due azioni di reduce a partire dalla seconda coppia E + E. Questo tipo di situazione è detta conflitto di tipo shift-reduce, in modo analogo è possibile avere anche conflitti di tipo reducereduce. 5 Nel caso Yacc rilevi un conflitto, produce lo stesso un parser effettuando delle scelte su quale azione intraprendere per prima. Le regole adottate per disambiguare tali conflitti sono: in un conflitto shift-reduce si da la precedenza all’azione di shift; in un conflitto reduce-reduce si da la precedenza alla regola che viene incontrata per prima. E’ sempre bene evitare i conflitti alla base. Questo è possibile riscrivendo la grammatica. Un altromodo per risolvere i conflitti, o per lo meno per pilotarne la risoluzione è quello di definire l’associatività dei simboli ambigui, tramite le istruzioni %rigth e %left da inserire nella sezione dichiarazioni. Esempio %rigth “=“ %left “+” “-” %left “*” “/” In questo caso si stabilisce l’associativita (se a destra o a sinistra) di =, +, - , *, / e inoltre si definisce anche che “+” e “-” anche se associano dallo stesso lato hanno una priorità minore. Esempio 6 Java Cup Accetta specifiche di un CFG e produce un parser LALR (1) parser (implementato in Java) con le routine relative alle azioni espressa in Java Simile a yacc, ma con alcuni miglioramenti (gestione dei nome) Di solito usato con JLex (o JFlex) 7 JavaCUP: Un generatore LALR per Java Definizione dei Tokens Espressioni Regolari Grammatica BNF-like Specification JavaCUP JLex Java File: Scanner Class Java File: Parser Class Riconoscimento dei Tokens Use lo Scanner per recuperare I Tokens Parses Stream of Tokens Syntactic Analyzer Passi per utilizzare JavaCup Scivere una specifica JavaCup (cup file) Definisce la grammatica e le azioni in un file (e.g., calc.cup) Eseguire JavaCup per generare il parser java java_cup.Main < calc.cup Nota il prefisso del package L’input è lo standard in Generera un parser.java e sym.java (nome default della clase, ma puo essere modificato) Scrivere il programma che usa il parser As esempio, UseParser.java Compilare ed eseguire il programma 8 Struttura della specifica Java Cup java_cup_spec ::= package_spec import_list code_part init_code scan_code symbol_list precedence_list start_spec production_list Cosa significa? Package_spec e import_list consente la gestione del naming Java Code e init_code permette l’inserimento di codice nell’output generato Scan code specifica come è invocato lo scanner (lexer) Symbol list e precedence list specificano I nomi dei temrinali e dei non-terminali e I nami e le loro precedence Start e production specifica la grammatica e l’assioma Specifica JavaCup (calc.cup) terminal PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN; terminal Integer NUMBER; non terminal Integer expr; precedence left PLUS, MINUS; precedence left TIMES, DIVIDE; expr ::= expr PLUS expr | expr MINUS expr | expr TIMES expr | expr DIVIDE expr | LPAREN expr RPAREN | NUMBER ; È la grammatica ambigua? Come possiamo ottenere PLUS ...? Sono i terminali restituita dallo scanner. Come per connettersi con lo scanner? 9 Ambiguous Grammar Error Se inseriamo la grammatica Expression ::= Expression PLUS Expression; Senza le precedenze JavaCUP segnalerà: Shift/Reduce conflict found in state #4 between Expression ::= Expression PLUS Expression . and Expression ::= Expression . PLUS Expression under symbol PLUS Resolved in favor of shifting. La grammatica è ambigua! Possiamo inserire in JavaCUP che PLUS è associativo a sinistra. Cconnesione con il parser Specifica dello scanner imports java_cup.runtime.*, Symbol, Scanner. corrispondente (calc.lex) implements Scanner import java_cup.runtime.*; next_token: definito in Scanner %% interface %implements java_cup.runtime.Scanner CalcSymbol, PLUS, MINUS, ... %type Symbol new Integer(yytext()) %function next_token %class CalcScanner %eofval{ return null; %eofval} NUMBER = [0-9]+ %% "+" { return new Symbol(CalcSymbol.PLUS); } "-" { return new Symbol(CalcSymbol.MINUS); } "*" { return new Symbol(CalcSymbol.TIMES); } "/" { return new Symbol(CalcSymbol.DIVIDE); } {NUMBER} { return new Symbol(CalcSymbol.NUMBER, new Integer(yytext()));} \r\n {} . {} 10 Run JLex java JLex.Main calc.lex Nota che il package prefix JLex Il programma generato calc.lex.java javac calc.lex.java La classe gnenerata: CalcScanner.class Generated CalcScanner class 1. import java_cup.runtime.*; 2. class CalcScanner implements java_cup.runtime.Scanner { 3. ... .... 4. public Symbol next_token () { 5. ... ... 6. case 3: { return new Symbol(CalcSymbol.MINUS); } 7. case 6: { return new Symbol(CalcSymbol.NUMBER, new Integer(yytext()));} ... ... 8. 9. } 10. } Interface Scanner è definita nel package java_cup.runtime public interface Scanner { public Symbol next_token() throws java.lang.Exception; } 11 Run javaCup Eseguire javaCup per generare il parser java java_cup.Main -parser CalcParser -symbols CalcSymbol < calc.cup Le classi generate: CalcParser; CalcSymbol; Compilare il parser javac CalcParser.java CalcSymbol.java CalcParserUser.java Usare il parser java CalcParserUser The token class Symbol.java 1. public class Symbol { 2. public int sym, left, right; 3. public Object value; 4. public Symbol(int id, int l, int r, Object o) { 5. this(id); left = l; right = r; value = o; 6. } 7. ... ... 8. public Symbol(int id, Object o) { this(id, -1, -1, o); } 9. public String toString() { return "#"+sym; } 10. } Instance variables: sym: il tipo del simbolothe symbol type; Lef (rigrh): la posizione a sinistra (desttra) nel file di input value: il valore lessicale the lexical value. Le azioni nel file lex : return new Symbol(CalcSymbol.NUMBER, new Integer(yytext()));} 12 CalcSymbol.java (default name is public class CalcSymbol { sym.java) public static final int MINUS = 3; 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. } public public public public public public public public static static static static static static static static final final final final final final final final int int int int int int int int DIVIDE = 5; NUMBER = 8; EOF = 0; PLUS = 2; error = 1; RPAREN = 7; TIMES = 4; LPAREN = 6; Contiene la dichiazione dei token declaration, ona per ogni token (terminale); E’ generata dalla lista dei terminali nel file cup terminal PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN; terminal Integer NUMBER è utilizzata dallo scanner per riferirsi ail tipo dei simboli (e.g., return new Symbol(CalcSymbol.PLUS);) f b l di ti Cl The program that uses the CalcParser import java.io.*; class CalcParserUser { public static void main(String[] args){ try { File inputFile = new File ("calc.input"); CalcParser parser= new CalcParser(new CalcScanner(new FileInputStream(inputFile))); parser.parse(); } catch (Exception e) { e.printStackTrace(); } } } 13 The program that uses the CalcParser l testo di input per essere analizzato può essere qualsiasi flusso di input (in questo esempio si tratta di un FileInputStream); Il primo passo è quello di costruire un oggetto parser. Un parser può essere costruito utilizzando uno scanner. Se non c'è alcuna segnalazione di errore, l'espressione nel file di input è corretto. } Valutazione dell’espressione La specifica precedente, indica solo il successo o il fallimento di un parser. Nessuna azione semantica è associata con le regole grammaticali. Per calcolare l'espressione, dobbiamo aggiungere il codice java nella grammatica di svolgere azioni in vari punti. Forma delle azioni semantiche: expr: e1 PLUS expr: e2 {: RESULT = new Integer (e1.intValue () + e2.intValue ());:} Azioni (codice Java) sono racchiusi all'interno di una coppia (::) Etichette E2, E2: gli oggetti che rappresentano il terminale o non terminale corrispondente; RESULT: Il tipo di risultato dovrebbe essere lo stesso del tipo di non-terminali corrispondenti. ad esempio, expr è di tipo Integer, così risultato è di tipo intege 14 Modifiche di calc.cup terminal PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN; terminal Integer NUMBER; non terminal Integer expr; precedence left PLUS, MINUS; precedence left TIMES, DIVIDE; expr ::= expr:e1 PLUS expr:e2 {: RESULT = new Integer(e1.intValue()+ e2.intValue()); :} | expr:e1 MINUS expr:e2 {: RESULT = new Integer(e1.intValue()- e2.intValue()); :} | expr:e1 TIMES expr:e2 {: RESULT = new Integer(e1.intValue()* e2.intValue()); :} | expr:e1 DIVIDE expr:e2 {: RESULT = new Integer(e1.intValue()/ e2.intValue()); :} | LPAREN expr:e RPAREN {: RESULT = e; :} | NUMBER:e {: RESULT= e; :} Modifiche di CalcParserUser import java.io.*; class CalcParserUser { public static void main(String[] args){ try { File inputFile = new File ("calc.input"); CalcParser parser= new CalcParser(new CalcScanner(new FileInputStream(inputFile))); Integer result= (Integer)parser.parse().value; System.out.println("result is "+ result); } catch (Exception e) { e.printStackTrace(); } } } Perché il risultato di parser().value è un intero? Ciò è determinato dal tipo di expr, che è il capo della prima produzione nelle specifiche javaCup: non terminal Integer expr; 15