Generatori di analizzatori sintattici

annuncio pubblicitario
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
Scarica