Python Language
Python Lex-Yacc
Sök…
Introduktion
PLY är en ren Python-implementering av de populära kompileringskonstruktionsverktygen lex och yacc.
Anmärkningar
Ytterligare länkar:
Komma igång med PLY
För att installera PLY på din maskin för python2 / 3, följ stegen nedan:
- Ladda ner källkoden härifrån .
- Packa upp den nedladdade zip-filen
- Navigera i den opackade
ply-3.10
mappen - Kör följande kommando i din terminal:
python setup.py install
Om du har slutfört alla ovanstående bör du nu kunna använda PLY-modulen. Du kan testa det genom att öppna en python-tolk och skriva import ply.lex
.
Obs: Använd inte pip
att installera PLY, det kommer att installera en trasig distribution på din maskin.
"Hej, världen!" av PLY - En enkel kalkylator
Låt oss visa kraften hos PLY med ett enkelt exempel: det här programmet kommer att ta ett aritmetiskt uttryck som en stränginmatning och försöka lösa det.
Öppna din favoritredigerare och kopiera följande kod:
from ply import lex
import ply.yacc as yacc
tokens = (
'PLUS',
'MINUS',
'TIMES',
'DIV',
'LPAREN',
'RPAREN',
'NUMBER',
)
t_ignore = ' \t'
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIV = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
def t_NUMBER( t ) :
r'[0-9]+'
t.value = int( t.value )
return t
def t_newline( t ):
r'\n+'
t.lexer.lineno += len( t.value )
def t_error( t ):
print("Invalid Token:",t.value[0])
t.lexer.skip( 1 )
lexer = lex.lex()
precedence = (
( 'left', 'PLUS', 'MINUS' ),
( 'left', 'TIMES', 'DIV' ),
( 'nonassoc', 'UMINUS' )
)
def p_add( p ) :
'expr : expr PLUS expr'
p[0] = p[1] + p[3]
def p_sub( p ) :
'expr : expr MINUS expr'
p[0] = p[1] - p[3]
def p_expr2uminus( p ) :
'expr : MINUS expr %prec UMINUS'
p[0] = - p[2]
def p_mult_div( p ) :
'''expr : expr TIMES expr
| expr DIV expr'''
if p[2] == '*' :
p[0] = p[1] * p[3]
else :
if p[3] == 0 :
print("Can't divide by 0")
raise ZeroDivisionError('integer division by 0')
p[0] = p[1] / p[3]
def p_expr2NUM( p ) :
'expr : NUMBER'
p[0] = p[1]
def p_parens( p ) :
'expr : LPAREN expr RPAREN'
p[0] = p[2]
def p_error( p ):
print("Syntax error in input!")
parser = yacc.yacc()
res = parser.parse("-4*-(3-5)") # the input
print(res)
Spara den här filen som calc.py
och kör den.
Produktion:
-8
Vilket är det rätta svaret för -4 * - (3 - 5)
.
Del 1: Tokenizing Input med Lex
Det finns två steg som koden från exempel 1 utförde: ett tokeniserade ingången, vilket betyder att den letade efter symboler som utgör det aritmetiska uttrycket, och det andra steget analyserades , vilket innebär att analysera de extraherade token och utvärdera resultatet.
Det här avsnittet ger ett enkelt exempel på hur man markerar användarinmatning och bryter sedan ned rad för rad.
import ply.lex as lex
# List of token names. This is always required
tokens = [
'NUMBER',
'PLUS',
'MINUS',
'TIMES',
'DIVIDE',
'LPAREN',
'RPAREN',
]
# Regular expression rules for simple tokens
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
# A regular expression rule with some action code
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
# Define a rule so we can track line numbers
def t_newline(t):
r'\n+'
t.lexer.lineno += len(t.value)
# A string containing ignored characters (spaces and tabs)
t_ignore = ' \t'
# Error handling rule
def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)
# Build the lexer
lexer = lex.lex()
# Give the lexer some input
lexer.input(data)
# Tokenize
while True:
tok = lexer.token()
if not tok:
break # No more input
print(tok)
Spara den här filen som calclex.py
. Vi kommer att använda detta när vi bygger vår Yacc-parser.
Bryta ner
Importera modulen med
import ply.lex
Alla lexers måste tillhandahålla en lista som heter
tokens
som definierar alla möjliga tokenamn som kan produceras av lexern. Denna lista krävs alltid.tokens = [ 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', ]
tokens
kan också vara en tupel av strängar (snarare än en sträng), där varje sträng betecknar ett symbol som tidigare.
Regex-regeln för varje sträng kan definieras antingen som en sträng eller som en funktion. I båda fallen bör variabelnamnet förinställas av t_ för att beteckna att det är en regel för matchande token.
För enkla tokens kan det reguljära uttrycket anges som strängar:
t_PLUS = r'\+'
Om någon typ av åtgärd behöver utföras kan en tokenregel anges som en funktion.
def t_NUMBER(t): r'\d+' t.value = int(t.value) return t
Observera att regeln anges som en doc-sträng i funktionen. Funktionen accepterar ett argument som är ett exempel på
LexToken
, utför en viss åtgärd och returnerar sedan argumentet.Om du vill använda en extern sträng som regex-regel för funktionen istället för att ange en doc-sträng, överväg följande exempel:
@TOKEN(identifier) # identifier is a string holding the regex def t_ID(t): ... # actions
En instans av
LexToken
objekt (låt oss kalla det här objektett
) har följande attribut:-
t.type
som är tokenstypen (som en sträng) (t.ex.:'NUMBER'
,'PLUS'
, etc). Som standard ärt.type
inställt på namnet efter prefixett_
. -
t.value
som är lexemet (den faktiska matchade texten) -
t.lineno
som är det aktuellat.lineno
(detta uppdateras inte automatiskt, eftersom lexern inte vet något om radnumren). Uppdatera linan med en funktion som hetert_newline
.
def t_newline(t): r'\n+' t.lexer.lineno += len(t.value)
-
t.lexpos
som är symbolens position relativt början på inmatningstexten.
-
Om ingenting returneras från en regex-regelfunktion kastas tokenet. Om du vill kassera ett token kan du alternativt lägga till t_ignore_-prefixet till en regex-variabel istället för att definiera en funktion för samma regel.
def t_COMMENT(t): r'\#.*' pass # No return value. Token discarded
...Är det samma som:
t_ignore_COMMENT = r'\#.*'
Detta är naturligtvis ogiltigt om du gör några åtgärder när du ser en kommentar. I vilket fall använder du en funktion för att definiera regex-regeln.
Om du inte har definierat ett symbol för vissa tecken men ändå vill ignorera det, använd
t_ignore = "<characters to ignore>"
(dessa prefix är nödvändiga):t_ignore_COMMENT = r'\#.*' t_ignore = ' \t' # ignores spaces and tabs
När man bygger masterregex lägger lex till de regexer som anges i filen enligt följande:
- Tokens definierade av funktioner läggs till i samma ordning som de visas i filen.
- Tokens definierade av strängar läggs till i minskande ordning för stränglängden på strängen som definierar regexet för det symbolet.
Om du matchar
==
och=
i samma fil, dra fördel av dessa regler.
Bokstäver är symboler som returneras som de är. Både
t.type
ocht.value
kommer att ställas in på själva karaktären. Definiera en lista med bokstäver som sådan:literals = [ '+', '-', '*', '/' ]
eller,
literals = "+-*/"
Det är möjligt att skriva tokenfunktioner som utför ytterligare åtgärder när bokstäver matchas. Du måste dock ställa in tokenstypen på lämpligt sätt. Till exempel:
literals = [ '{', '}' ] def t_lbrace(t): r'\{' t.type = '{' # Set token type to the expected literal (ABSOLUTE MUST if this is a literal) return t
Hantera fel med t_error-funktionen.
# Error handling rule def t_error(t): print("Illegal character '%s'" % t.value[0]) t.lexer.skip(1) # skip the illegal token (don't process it)
I allmänhet
t.lexer.skip(n)
över n tecken i inmatningssträngen.
Slutliga förberedelser:
Bygg lexern med
lexer = lex.lex()
.Du kan också placera allt i en klass och ringa instans för klassen för att definiera lexern. T.ex:
import ply.lex as lex class MyLexer(object): ... # everything relating to token rules and error handling comes here as usual # Build the lexer def build(self, **kwargs): self.lexer = lex.lex(module=self, **kwargs) def test(self, data): self.lexer.input(data) for token in self.lexer.token(): print(token) # Build the lexer and try it out m = MyLexer() m.build() # Build the lexer m.test("3 + 4") #
Ge inmatning med
lexer.input(data)
där data är en strängFör att få tokens använder du
lexer.token()
som returnerar matchade token. Du kan iterera över lexer i en slinga som i:for i in lexer: print(i)
Del 2: Analysera inmatad inmatning med Yacc
Detta avsnitt förklarar hur den tokeniserade ingången från del 1 bearbetas - den görs med hjälp av Context Free Grammars (CFG). Grammatiken måste anges och tokens behandlas enligt grammatiken. Under huven använder tolkaren en LALR-tolkare.
# Yacc example
import ply.yacc as yacc
# Get the token map from the lexer. This is required.
from calclex import tokens
def p_expression_plus(p):
'expression : expression PLUS term'
p[0] = p[1] + p[3]
def p_expression_minus(p):
'expression : expression MINUS term'
p[0] = p[1] - p[3]
def p_expression_term(p):
'expression : term'
p[0] = p[1]
def p_term_times(p):
'term : term TIMES factor'
p[0] = p[1] * p[3]
def p_term_div(p):
'term : term DIVIDE factor'
p[0] = p[1] / p[3]
def p_term_factor(p):
'term : factor'
p[0] = p[1]
def p_factor_num(p):
'factor : NUMBER'
p[0] = p[1]
def p_factor_expr(p):
'factor : LPAREN expression RPAREN'
p[0] = p[2]
# Error rule for syntax errors
def p_error(p):
print("Syntax error in input!")
# Build the parser
parser = yacc.yacc()
while True:
try:
s = raw_input('calc > ')
except EOFError:
break
if not s: continue
result = parser.parse(s)
print(result)
Bryta ner
Varje grammatikregel definieras av en funktion där doktringen till den funktionen innehåller lämplig kontextfri grammatikspecifikation. Uttalelserna som utgör funktionsorganet genomför de semantiska handlingarna i regeln. Varje funktion accepterar ett enda argument p som är en sekvens som innehåller värdena för varje grammarsymbol i motsvarande regel. Värdena på
p[i]
mappas till grammatikssymboler som visas här:def p_expression_plus(p): 'expression : expression PLUS term' # ^ ^ ^ ^ # p[0] p[1] p[2] p[3] p[0] = p[1] + p[3]
För symboler är "värdet" för motsvarande
p[i]
detsamma som attributetp.value
tilldelats i lexer-modulen. SåPLUS
kommer att ha värdet+
.För icke-terminaler bestäms värdet av vad som är placerat i
p[0]
. Om ingenting placeras är värdet Inget. Dessutom ärp[-1]
inte samma sak somp[3]
, eftersomp
inte är en enkel lista (p[-1]
kan ange inbäddade åtgärder (diskuteras inte här)).
Observera att funktionen kan ha valfritt namn, så länge den föregås av p_
.
p_error(p)
definieras för att fånga syntaxfel (samma somyyerror
i yacc / bison).Flera grammatikregler kan kombineras till en enda funktion, vilket är en bra idé om produktioner har en liknande struktur.
def p_binary_operators(p): '''expression : expression PLUS term | expression MINUS term term : term TIMES factor | term DIVIDE factor''' if p[2] == '+': p[0] = p[1] + p[3] elif p[2] == '-': p[0] = p[1] - p[3] elif p[2] == '*': p[0] = p[1] * p[3] elif p[2] == '/': p[0] = p[1] / p[3]
Teckenbokstäver kan användas istället för symboler.
def p_binary_operators(p): '''expression : expression '+' term | expression '-' term term : term '*' factor | term '/' factor''' if p[2] == '+': p[0] = p[1] + p[3] elif p[2] == '-': p[0] = p[1] - p[3] elif p[2] == '*': p[0] = p[1] * p[3] elif p[2] == '/': p[0] = p[1] / p[3]
Naturligtvis måste bokstäverna anges i lexer-modulen.
Tomma produktioner har formen
'''symbol : '''
För att uttryckligen ställa in startsymbolen, använd
start = 'foo'
, därfoo
är någon icke-terminal.Inställning av företräde och associativitet kan göras med hjälp av företrädesvariabeln.
precedence = ( ('nonassoc', 'LESSTHAN', 'GREATERTHAN'), # Nonassociative operators ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), ('right', 'UMINUS'), # Unary minus operator )
Tokens beställs från lägsta till högsta prioritet.
nonassoc
betyder att dessa symboler inte associerar. Detta innebär att något soma < b < c
är olagligt medana < b
fortfarande är lagligt.parser.out
är en felsökningsfil som skapas när yacc-programmet körs för första gången. När en skiftning / minskning av konflikt inträffar växlar tolkaren alltid.