Implementazione di Linguaggi 2 Massimo Ancona Testo: A.V. Aho, R. Sethi, J.D.Ullman Compilers Principles,Techniques and Tools, Addison Wesley
Diagramma del processo di compilazione
Strumenti associati al compilatore: il Linker
Contesto/ambiente di un cmp Programming Environment Un ambiente grafico interattivo per lo sviluppo, il testing e la manutenzione dei programmi: integra e gestisce gli strumenti e i meccanismi seguenti Strumenti e meccanismi correlati ai compilatori Sistemi per le compilazioni separate Sistemi per il controllo delle versioni Meccanismi per le ricompilazioni efficienti Macroprocessori e Preprocessori Linker loader Interpreti debugger profiler
Processo Di Compilazione Nella sua forma piu' astratta il processo di compilazione e' descritto nella figura seguente: il compiler e' una funzione che mappa un programma in linguaggio sorgente in un programma equivalente in linguaggio oggetto.
Fasi e Passi (Passate) Sono due concetti ortogonali: Le fasi denotano trasformazioni sul programma eseguite da tutti i compilatori. I passi si riferiscono al numero di processi successivi, in cui e' suddiviso un compilatore (il loro numero dipende in parte dal linguaggio). Il compilatore in una passata, legge il codice sorgente o una sua forma intermedia equivalente, raffinando il processo di treduzione e producendo una nuova rappresentazione intermedia, fino ad arrivare al codice target, nell’ultima passata.
Fasi Di Compilazione Una fase denota il tipo di operazione effettuata. Fasi e passate sono concetti ortogonali anche se una struttura canonica del compilatore tende ad incapsulare una (o piu') fasi, in una specifica passata. La divisione piu' grossolana prevede due fasi: analisi e sintesi. Con la prima il programma sorgente viene suddiviso nelle parti costituenti ottenendone una rappresentazione intermedia. Nella seconda si costruisce il programma target nel linguaggio oggetto.
Analisi e Sintesi Strumenti che eseguono l’analisi dell’input Editori di testo, Formattatori e Sistemi Ipertestuali (HTML, XML, PDF, TEX) Silicon Compilers (sintetizzatori di circuiti e componenti Hardware) Query Interpreters Motori di Ricerca e strumenti correlati
Analisi e Sintesi Analisi e Sintesi Analisi divide il pgm sorgente nelle parti costituenti creando una rappresentazione intermedia. Sintesi costruisce il programma oggetto dalla rappresentazione intermedia.
Dettaglio Delle Fasi ELenco completo delle possibili fasi di compilazione: Concettualmente un compilatore opera in fasi, e ogni fase trasforma il programma sorgente da una rappresentazione ad un’altra. Questa è una tipica DECOMPOSIZIONE di un compilatore. Le fasi sulla ragruppate sulla sinistra “lessicale sintattica e semantica” rappresentano la porzione di analisi del compilatore, mentre le restanti tre fasi sulla destra rappresentano la sintesi del compilatore, in cui viene effettivamente costruito il codice oggetto partendo dalla rappresentazione intermedia.
Rappresentazioni Intermedie sorgente primo := base + 53 * delta; out lessicale id[1] op= id[2] op+ ci=53 op* id[3] out sintattico op= id[1] op+ id[2] op* 53 id[3]
Rappresentazioni Intermedie (e Finali) out semantica op= id[1] op+ id[2] op* float id[3] 53 Il risultato della fase lessicale viene detto parse tree, ma una rappresentazione interna più comune è data dal SYNTAX TREE. Che è una rappresentazione compressa del parse tree in cui gli operatori a
Rappresentazioni Intermedie codice intermedio tmp1 = float(53) tmp2 = id[3] op* tmp1 tmp3 = id[2] op+ tmp2 id[1] = tmp3 Ottimizzazione di codice (intermedio) tmp1 = id[3] op* 53.00 id[1] = id[2] op+ tmp1 Dopo la fase di analisi alcuni compilatori generano una rappresentazione intermedia esplicita del sorgente. Questa rappresentazione intermedia deve avere 2 caratteristiche: Essere facile da produrre E facile da tradurre nel codice oggetto. Il codice intermedio può avere varie forme – la rappresentazione utilizzata qui è detta THREE ADDRESS CODE. Consiste di una serie di istruzioni, ognuna delle quali ha al più tre operandi. La fase di ottimizzazione del codice cerca di migliorare il codice intermedio per renderlo di più veloce esecuzione. Di solito nel codice intermedio viene generata 1 istruzione per ogni operatore presente della rappresentazione ad albero dopo l’analisi semantica
Rappresentazione Finale codice macchina: movf id3, rf2 mulf #53.00, rf2 movf id[2], rf1 addf rf2, rf1 movf rf1, id[1] Supponiamo di utilizzare solamente due registri rf1 e rf2. L’ultima fase del compilatore è la generazione del codice target, assembly. Per prima cosa viene selezionata una locazione di memoria per ogni variabile utilizza nel programma, ogni istruzione intermedia viene tradotta in una sequenza di istruzioni macchina che compiano lo stesso task. Un aspetto cruciale è l’ASSEGNAMENTO DELLE VARIABILI AI REGISTRI. Il primo operando di ogni istruzione specifica il sorgente ed il secondo la destinazione, la F presente in ogni istruzione indica che stiamo lavorando con numeri floating-point.
Tipi di Compilatori Vi sono diversi tipi di compilatori Ad un sol passo Multi-passo Load_and_go Debugging Optimizing Compiler-Interpreter Effettuano una sola passata sul codice sorgente. Eseguono più passate del codice sorgete o del albero sintattico I compilatori load-and-go traducono in codice sorgente in istruzioni macchina che vengono direttamente memorizzate nella memoria del computer, una volta tradotto il programma è eseguito.
Introduzione alla Compilazione Un compilatore in due passate
Le fasi di analisi del sorgente Analisi Lessicale. I caratteri del codice sorgente vengono letti left-to-right e raggruppati in Token. Analisi Sintattica. I caratteri o token sono raggruppati in frasi grammaticali. Analisi Semantica. Vengono effettuati controlli per trovare errori semantici e raccogliere informazioni per la generazione di codice.
Le fasi di analisi del sorgente Analisi Lessicale. Token Source Program Lexical analyzer Parser Get next Token La fase di analisi lessicale è la prima fase del compilatore, il compito principale è leggere i caratteri in input e produrre in output una serie di token che il parser utilizzerà per l’analisi sintattica. Come compito secondario: Toglie i commenti dal programma sorgente Toglie i caratteri degli spazi bianchi, tab e new line Può tenere traccia del numero di linee, in questo modo il numero di linea può essere associato ai messaggi di errore. Alcune volte l’analizzatore lessicale ha anche il compito di copiare il programma sorgente con gli errori marcati Alcuni analizzatori lessicali sono suddivisi in due fasi a cascata: prima fase detta scanning e seconda fase analisi lessicale. Symbol Table
Le fasi di analisi del sorgente Analisi Lessicale. Eseguita dallo Scanner Ad esempio l’istruzione: alpha:=beta+gamma*100; viene codificata nei seguenti TOKEN: <ID:’alpha’><OP::=><ID:’beta’><OP:><ID:’gamma’><OP:><VAL:’100’> Gli spazi ridondanti, i fine linea ecc. che separano i token vengono eliminati.
Le fasi di analisi del sorgente Analisi Sintattica. Ogni linguaggio di programmazione ha delle regole che descrivono la struttura sintattica di un programma ben formato. La sintassi di un linguaggio di programmazione può essere descritta da una grammatica libera da contesto
Le fasi di analisi del sorgente Analisi Sintattica. Token Source Program Lexical analyzer Parser Parse Tree Get next Token Nel nostro modello, il parser ottiene in input una sequenza di token dall’analizzatore lessicale, e verifica che tale sequenza di token possa essere generata dalla grammatica del linguaggio sorgente. Symbol Table
Le fasi di analisi del sorgente Analisi Sintattica. Eseguita dal Parser Agisce dopo l’analisi lessicale. Raggruppa i token in frasi grammaticali che si rappresentano con un parse tree. Parse tree di: alpha := beta + gamma * 100 (<:=>) <EXP:ID> <EXP: > <ID:’alpha’> <EXP:ID> <EXP: > <ID:’beta’> <EXP:ID> <EXP:VAL> <ID:’gamma’> <VAL:’100’>
Le fasi di analisi del sorgente Analisi Semantica. Controlla il codice sorgente alla ricerca di errori semantici e raccoglie informazioni sui tipi per la fase di generazione di codice Type checking è la componente più importate di questa fase: Controlla che gli operatori abbiano operandi permessi dalla specifica del linguaggio.
Relazione tra le Fasi La suddivisione tra analisi lessicale e sintattica è piuttosto arbitraria (come per le altre fasi della compilazione); Uno dei metodi usati per discriminarle si basa sulla ricorsione: i costrutti descrivibili senza ricorsione si assegnano all’analisi lessicale; quelli sostanzialmente ricorsivi a quella sintattica.
Sintassi Formale dei Linguaggi di Programmazione La si specifica tramite: diagrammi sintattici; equazioni BNF EBNF; grammatiche non contestuali (context free) Tutte forme sostanzialmente equivalenti. La sintassi dei linguaggi di programmazione viene in generale specificata mediante DIAGRAMMI SINTATTICI o EQUAZIONI BNF, o tramite GRAMMATICHE LIBERE DA CONTESTO.
Grammatiche Libere da Contesto (CFG) Le Context Free Grammar sono una notazione per specificare la sintassi di un linguaggio. Una CFG ha 4 componenti: Un insieme di token, detti simboli terminali Un insieme di non-terminali Un insieme di produzioni, dove ogni produzione consiste di un non-terminale (left side), una freccia e una sequenza di token e/o non-terminali (right side) L’indicazione di un non-terminale come simbolo iniziale I token o simboli terminali sono ad esempio le parole chiave del linguaggio. I non-terminali sono di variabili sintattiche che denotano un insieme di stringhe. Le produzioni specificano il modo in cui i terminali ed i non-terminali possono essere combinati per formate stringhe.
Grammatiche Libere da Contesto Esempio Vediamo un esempio di produzioni che definiscono semplici espressioni aritmetiche: expr expr op expr expr ( expr ) expr id op + op - op * op / op Simboli terminali: id + - * / ( ( Simboli non-terminali: expr op Simbolo iniziale: expr
Grammatiche Libere da Contesto Esempio Scrivere la grammatica context free per esprimere zero o più volte il simbolo (terminale) thing. List List thing List List List thing List List List List List List thing
Grammatiche Libere da Contesto (CFG): Definizione Formale Alfabeto T () : è un insieme finito non vuoto, e gli elementi di T () vengono chiamati simboli o caratteri. Useremo le lettere V, T, N, per indicare alfabeti
Grammatiche Libere da Contesto (CFG): Definizione Formale Parola di lunghezza k0, su un alfabeto T: è una sequenza finita w=x1x2…xk di elementi di T, k, denotata anche k=|w| è detta lunghezza di w, e la parola di lunghezza zero, detta parola vuota, e’ indicata .
Grammatiche Libere da Contesto (CFG): Definizione Formale L’insieme delle parole su T (linguaggio), indicato con T* monoide: con legge di composizione concatenazione di parole Date w1,w2T, w1= x1x2…xk, w2= y1y2…yl w= w1.w2= x1x2…xky1y2…yl, di lunghezza k+l, è detta concatenazione di w1 e w2. Il monoide (T*,.) è detto monoide libero su T.
Parentesi Algebrica: Monoide Un monoide è un insieme M munito di una singola operazione binaria, chiamata prodotto, che ad ogni coppia di elementi a, b di M associa un elemento ab, rispettando i seguenti assiomi: per ogni a, b appartenenti a M, il loro prodotto ab appartiene ancora a M, vale a dire, M è chiuso rispetto al prodotto. Il prodotto è associativo: dati a, b, c appartenenti a M, vale (ab)c = a(bc). Esiste in M un (unico) elemento neutro e tale che ae = ea = a.
Grammatiche Libere da Contesto (CFG): Definizione Formale Una CFG formalmente è una quadrupla G=(N,T,P,S) con NT= e V=NT, dove: T è un alfabeto di simboli terminali di G. N è un alfabeto di simboli non-terminali di G. P è un insieme finito di produzioni di G. SN è detto simbolo iniziale di G.
Grammatiche Libere da Contesto (CFG): Definizione Formale Notazione utilizzata per le produzioni NV*: a,b,c,…,+,-,…,0,1,…,(,[,{,… indicano elementi di T (= terminali) A,B,C,… indicano elementi di N (= non-terminali) U,V,X,Y,Z indicano elementi di V=TN u,v,x,y, indicano elementi di T*(= stringhe di terminali) ,,,…, indicano elementi di V* (= stringhe di simboli grammaticali) =(A,)NV* è indicata da A
Linguaggi Context Free Linguaggio generato da G: L(G)=wT*| S * w Un linguaggio L T* è libero da contesto se esiste G=(N,T,P,S) CFG tale che L=L(G)
A (Derivazione) Forme Sentenziali Data G=(N,T,P,S) una forma sentenziale di G è definita ricorsivamente, come segue: S è una forma sentenziale di G, se A è una forma sentenziale di G e A è una produzione di G allora è una forma sentenziale di G. La relazione tra A e al punto precedente viene indicata: A (Derivazione)
Derivazioni Dirette e Non A esprime che deriva direttamente da A o che A genera direttamente . Le notazioni * + indicano rispettivamente la chiusura transitiva-riflessiva e transitiva di * denota che = oppure i i=1..n =1 2 … n = e n>0. + denota che =1 2 … n = e n>0. Idea di base: le produzioni di una grammatica possono essere viste come REGOLE DI RISCRITTURA in cui il NON-TERMINALE a sinistra viene rimpiazzato dalla stringa della parte destra della produzione.
Derivazioni - Esercizio La stringa – ( id + id ) è una sentenza della grammatica G? E - E - ( E ) - ( E + E ) - ( id + E ) - ( id + id ) G: E E + E E E * E E ( E ) E - E E id E * - ( id + id )
Derivazioni canoniche Una derivazione * e’ detta canonica destra e indicata rm* , se ad ogni passo di derivazione si espande il simbolo non-terminale più a destra. rm* se i si ha i=iAixi, i+1=iixi, Ai i, =1 2 … n = i=1,…,n
Derivazioni canoniche Una derivazione * e’ detta canonica sinistra e indicata lm* , se ad ogni passo di derivazione si espande il simbolo non-terminale più a sinistra. lm* se i si ha i=xiAi i, i+1=xii i, Ai i, =1 2 … n = i=1,…,n
Derivazioni Canoniche Esempio Non-terminali terminali G=({E,T,P}, {(,),a,b,c,*,+,-}, P, E) P={EE +T |E -T |T, T T *P |P, P (E ) | a | b | c} Data (a-b) – c la derivazione: E E -T T -T P -T (E )-T (E - T )-T (T - T )-T (P - T )-T (a - T )-T (a - P )-T (a - b )-T (a - b )-P (a - b ) - c È canonica sinistra.
Esempio Sia G=({E,T,P}, {(,),a,b,c,*,+,-}, P, E) dove P={EE+T | E-T | T, T T*P | P, P (E) | a | b |c} La derivazione: E E-T E-P E-c T-c P-c (E)-c (E-T)-c (E-P)-c (E-b)-c (T-b)-T (P-b)-c (a-b)-c è canonica destra, confrontandola con la canonica sinistra: E E-T T-T P-T (E)-T (E-T)-T (T-T)-T (P-T)-T (a-T)-T (a-P)-T (a-b)-T (a-b)-P (a-b)-c Si vede che differiscono solo per l’ordine di applicazione delle produzioni. Entrambe applicano le stesse produzioni alle stesse istanze di non terminali
Parsing Tree Tutte le derivazioni che differiscono solo per l’ordine di applicazione delle produzioni sono sostanzialmente equivalenti e possono essere rappresentate da un’unica derivazione canonica destra, un’ unica derivazione canonica sinistra o da un unico albero etichettato detto Parsing Tree. Il parse tree può essere visto come una rappresentazione grafica della derivazione, che filtra le scelte fatte a riguardo dell’ordine di applicazione delle produzioni.
Parsing Tree - Definizione La radice è etichettata da S Ogni foglia dell’albero è etichettato da o da un simbolo di T (terminale) Ogni nodo interno è etichettato da un simbolo di N (non terminale) Se A etichetta un nodo interno e X1,X2,…, Xn sono le etichette dei figli, allora AX1X2…Xn P
Parsing Tree - Esempio Derivazione: - (id + id) E - E ( E ) E + E
Esempio di Parsing Tree Derivazione di: (a – b ) – c Grammatica: EE +T |E -T |T T T *P |P, P (E ) | a | b | c}
Classificazione di Chomsky È una classificazione dei linguaggi basata sulla complessità delle produzioni delle grammatiche che li generano. I linguaggi di tipo 0 sono generati da grammatiche a struttura di frase. I linguaggi di tipo 1 sono generati da grammatiche dipendenti dal contesto. I linguaggi di tipo 2 sono generati da grammatiche libere dal contesto. I linguaggi di tipo 3 sono generati da grammatiche lineari (o regolari).
Classificazione di Chomsky Grammatiche a struttura di frase. Le produzioni sono del tipo A Grammatiche dipendenti dal contesto. Le produzioni sono del tipo A dove , V*, AN e V* . Grammatiche libere dal contesto. Le produzioni sono del tipo A, dove V* . Grammatiche lineari (o regolari). Le produzioni sono del tipo AuB oppure Au, dove u * e A, B N.
Classificazione di Chomsky Ogni grammatica di tipo n è anche una grammatica di tipo n − 1. Un linguaggio è di tipo n se è generato da una grammatica di tipo n, ma non di tipo n + 1. Esempi: - Linguaggio di tipo 3: L = {ambn : m, n 0}. - Linguaggio di tipo 2: L = {anbn : n 0}. - Linguaggio di tipo 1: L = {anbncn : n 0}. - Linguaggio di tipo 0: L = {an : n è numero primo}.
Grammatiche e Linguaggi Ambigui Una grammatica G è ambigua se: wL(G) con due parsing tree diversi Un linguaggio è inerentemente ambiguo se per ogni G tale che L=L(G) si ha che G e’ ambigua.
Tecniche per Disambiguare Inserire regole di precedenza per gli operatori. Utilizzare associatività a sinistra o a destra. Manipolare la grammatica.
Grammatica Ambigua Esempio G=({S}, {if, then, else, s, b}, P={S if b then S | if b then S else S | s}, S) Srm if b then S rm if b then if b then S else S rm if b then if b then S else s rm if b then if b then s else s S rm if b then S else S rm if b then S else s rm if b then if b then S else s rm if b then if b then s else s if b then if b then s else s
Grammatica Ambigua Esempio Grammaticha ambigua: G=({S}, {if, then, else, s, b}, P={S if b then S | if b then S else S | s}, S) Grammatica disambiguata: G=({S, S1, S2}, {if, then, else, s, b}, P={S S1, S1 if b then S1 | if b then S2 else S1 | s, S2 if b then S2 else S1 | s},
Grammatica non ambigua con proprietà opportune Nota: la gramamtica è context free, e mostriamo solo le produzioni. Grammatica di espressioni non ambigua con priorità di operatori e associatività cablate EE+T | E-T | T TT*F | T/F | F FP^F | P P (E) | I | N I a|b|c|d N 0 | 1 | 2 G per espressioni, semplice ma ambigua EEOE|(E)|a|b|c|d O+|-|*|^
Altri esempi di grammatiche G0 di espressioni non ambigua con priorità di operatori e associatività cablate EE+T | E-T | T |-T|+T TT*F | T/F | F F (E) | I | N I a|b|c|d N 0 | 1 | 2 G1 per espressioni, semplice, non ambigua ma flat EEOT|T|-T|+T T (E)|a|b|c|d|0|1|2 O+|-|*|/
Derivazione canonica di G0 Data a-b*c e G0 abbiamo: ErmE-T rmE-T*F rmE-T*I rmE-T*c rmE-F*c rmE-I*c rmE-b*c rmT-b*c rmF-b*c rmI-b*c rma-b*c EE+T | E-T | T |-T|+T TT*F | T/F | F F (E) | I | N I a | b | c | d N 0 | 1 | 2
Parse tree (G0) Derivazione Canonica: ErmE-T rmE-T*F rmE-T*I rmE-T*c rmE-F*c rmE-I*c rmE-b*c rmT-b*c rmF-b*c rmI-b*c rma-b*c E E - T T T * F F F I I I c a b
Derivazione canonica di G1 Data a-b*c e G1 abbiamo: ErmEOT rmEOc rmE*c rmEOT*c rmEOb*c rm rmE-b*c rm rmT-b*c rm a-b*c EEOT|T|-T|+T T (E)|a|b|c|d|0|1|2 O+|-|*|/
Parse tree (G1) Derivazione Canonica: ErmEOT rmEOc rmE*c rmEOT*c rmEOb*c rm rmE-b*c rm rmT-b*c rm a-b*c E E O T E O T * c T - b a
Esercizio Studiare G2: EFOT | T FFOT | T | T(E)| a | b | c | d | 0 | 1 | 2 O+ | - | * | ^
Esempio Documenti HTML XML: DPs B Pd Ps<html> Pd<\html> B <body>L <\body> LE L | E | E<div>R<\div> Rrest
Esempio Documenti HTML XML: DPs B Pd Ps<html> Pd<\html> B <body>L <\body> LE L | E | E<div>R<\div> Rrest
Esempio Documenti HTML XML: DPs B Pd Ps<html> Pd<\html> B <body>L <\body> LE L | E | E<div>R<\div> Rrest
Esempio Documenti HTML XML: DPs L Pd Ps<html> Pd<\html> LE L | E | E<div>R<\div> Rrest
Operazioni sui Linguaggi Dati due linguaggi L, M * definiamo prodotto di L e M, indicato LM o L.M, l’insieme: LM ={uv | u L & v M}
Operazioni sui Linguaggi Definiamo chiusura di Kleene di L, indicata con L*, l’operazione: L0={} L1=L Li=LLi-1 i>0 L*=i0 Li L+=i>0 Li
Operazioni sui Linguaggi - Esempi Proprieta’: supponiamo T terminale e vediamo le produzioni di E G0=({E},{t,+,-},P0,E) P0={EE+t | E-t | t|-t|+t} L(G0)={-t,+t,t}({+,-}t)* e G1=({E,O},{t,+,-},P1,E) P1={EEOt|t|-t|+t,O+|-} O+|- L(G1)=L(G0)
Linguaggi - Esempi Esempio di linguaggio inerentemente ambiguo L={aibjck| i=j OR j=k; i,j,k0} SS1S2 | S3S4 S1aS1b | S2 S2 c| S4bS4c | S3 S3 a| Esempio di linguaggio non context free. L={anbncn| n>() 0} non e’ un CFL
Syntax Tree (albero sintattico) e Parsing Tree Una delle rappresentazioni intermedie più usate. Più astratto del parsing tree, dipende solo dal linguaggio (non dalla grammatica)
EBNF Usa i simboli metalinguistici “=“ “|” “[“ “]” “{“. “}” “.” “”” […] significa opzionalita’ {…} significa ripetizione: 0 o piu’ volte “(…)” significa raggruppamento “|” indica alternativa “.” termina una produzione I simboli terminali vengono racchiusi tra “ “ Esempio Expression = SimpleExpression [ Relation SimpleExpression ]. Relation = "=" | "<>" | "<" | "<=" | ">" | ">=" | "IN". SimpleExpression = [ "+" | "-" ] Term { AddOperator Term }. AddOperator = "+" | "-" | "OR". Term = Factor { MulOperator Factor }. MulOperator = "*" | "/" | "DIV"| "MOD" | "AND"
EBNF Esempio (Espressioni) Expression = SimpleExpression [ Relation SimpleExpression ]. Relation = "=" | "<>" | "<" | "<=" | ">" | ">=" | "IN". SimpleExpression = [ "+" | "-" ] Term { AddOperator Term }. AddOperator = "+" | "-" | "OR". Term = Factor { MulOperator Factor }. MulOperator = "*" | "/" | "DIV" | "MOD" | "AND"
EBNF Espressioni (cont.) Designator [ ActualParameters ] | "(" Expression ")" | NOT Factor. Set = "[" [ Element { "," Element } ] "]". Element = OrdinalConstant [ ".." OrdinalConstant]. OrdinalConstant= Char | Integer. ActualParameters = ["(" [ ExpressionList ] ")"]. Ident = IdChar { IdChar | Digit }. IdChar = Letter | "_". Number = Integer | Real.
EBNF Espressioni (cont. 2) Integer = Digit { Digit } | Digit { HexDigit } "H". Real = Digit { Digit } "." { Digit } [ ScaleFactor ]. ScaleFactor = "E" [ "+" | "-" ] Digit { Digit }. HexDigit = Digit | "A" | "B" | "C" | "D" | "E" | "F". Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9". CharConstant = "'" Char "'" | Digit { HexDigit } "X". String = ' { CharN | "''" } '. Char = CharN | "'".