Leksička analiza¶
Program (izvorni kod) nam je najčešće zadan kao string, dakle niz znakova Unikoda. Prvo što trebamo učiniti s njime je prepoznati osnovne podnizove koji predstavljaju podlogu za dalju obradu. Ti podnizovi tvore tokene. Iako može biti beskonačno mnogo tokena (npr. u mnogim programskim jezicima, svako legalno ime varijable je mogući token), ključno je da se svi oni razvrstavaju u konačno mnogo tipova.
Tipovi tokena su ono što zapravo upravlja kasnijom fazom sintaksne
analize programa. To znači da, idealno, u sintaksnoj analizi ne bi
smjelo biti bitno koji je točno niz znakova na određenom mjestu u
programu (to zovemo sadržaj tokena), već samo njegov tip. Odnosno,
ako je program sintaksno ispravan, ostat će takav i ako bilo koji token
u njemu zamijenimo tokenom drugog sadržaja a istog tipa (recimo, broj
3
zamijenimo brojem 58
, ili ime varijable xy
zamijenimo imenom t
).
Pored tipa i sadržaja, tokeni prepoznati u izvornom kodu imaju brojne metapodatke koji kazuju gdje token počinje (u kojem retku odnosno stupcu izvornog koda), gdje završava, je li uspješno obrađen od strane sintaksnog analizatora itd. No svaki token mora imati tip i sadržaj, koji su na tokenu t
dostupni kao t.tip
i t.sadržaj
.
Tipovi tokena¶
Da bismo započeli leksičku analizu, moramo definirati enumeraciju — klasu koja nabraja sve moguće tipove tokena. U vepru postoji bazna klasa TipoviTokena
nasljeđivanjem od koje dobijemo svu potrebnu funkcionalnost. Evo primjera:
from vepar import *
class T(TipoviTokena):
PLUS = '+'
MINUS, PUTA, KROZ = '-*/'
STRELICA, RAZLIČITO = '->', '!='
class BROJ(Token):
def vrijednost(t): return int(t.sadržaj)
class I(Token):
literal = 'i'
def vrijednost(t): return 1j
class IME(Token): pass
Upravo navedeni primjer pokazuje nekoliko mogućnosti za definiranje tokena, od jednostavnijih prema složenijima.
obične inertne tokene koji uvijek (ili gotovo uvijek) imaju isti sadržaj (literale) definiramo navođenjem tipa lijevo od
=
i sadržaja desno. Dakle, token tipaT.PLUS
ima podrazumijevani sadržaj'+'
.više literala možemo definirati koristeći Pythonovo ugrađeno raspakiravanje stringova (za jednoznakovne tokene) i slogova (za višeznakovne). Dakle, token tipa
T.PUTA
ima podrazumijevani sadržaj'*'
, a token tipaT.RAZLIČITO
ima podrazumijevani sadržaj'!='
.tokene koji nisu inertni (imaju metode koje sudjeluju u semantičkoj analizi) definiramo kao unutarnje klase koje nasljeđuju klasu
Token
. TakoT.BROJ
nema podrazumijevani sadržaj, ali jednom kad ga prepoznamo i podvrgnemo semantičkoj analizi, moći ćemo iz njegovog sadržaja dobiti njegovu vrijednost kao Pythonovint
.ako želimo neinertni literal (ima metode za semantičku analizu, a također ima konstantni sadržaj), možemo takvoj unutarnjoj klasi dati atribut
literal
. Tako se za potrebe prepoznavanjaT.I
ponaša kao da je definiran sI = 'i'
, a u semantičkoj analizi za bilo koji tokent
tipaT.I
možemo pozvatit.vrijednost()
i dobiti Pythonov kompleksni broj1j
(imaginarnu jedinicu).u suprotnom smjeru, ako želimo inertni token bez podrazumijevanog sadržaja, možemo jednostavno definirati unutarnju klasu s
pass
(ili...
ako želimo signalizirati da je situacija privremena i da ćemo kasnije možda dodati neku semantiku za taj tip tokena).
Lexer¶
Lexer je funkcija (generator u Pythonu) koja prolazi kroz ulazni
string i redom daje (yield
) tokene koje u njemu prepoznaje. Vepar zahtijeva da bude dekorirana
s @lexer
. Jedini argument joj se obično zove lex
, i predstavlja objekt klase Tokenizer
čije metode pomažu pri leksičkoj analizi. Ovdje navodimo samo najčešće načine korištenja lex
a — za potpunije informacije pogledajte dokumentaciju (recimo pomoću help(Tokenizer)
).
Lexer najčešće počinje linijom poput for znak in lex:
, dakle sastoji se od petlje koja u svakom prolazu varijabli znak
pridružuje sljedeći znak ulaza. Nakon toga obično slijedi grananje s obzirom na to koji odnosno kakav znak smo našli. To utvrđujemo najčešće običnom usporedbom poput znak == '$'
ili pozivom metode klase str
poput znak.islower()
(što se može zapisati i kao str.islower(znak)
). Često korištene metode ovdje su:
str.isspace
Je li znak bjelina, poput razmaka, tabulatora, prelaska u novi red i sličnog.
str.isalpha
Je li znak slovo, poput
A
,č
,lj
, ili是
. Ako želite samo ASCII-slova, provjeriteznak in string.ascii_letters
, ali najčešće za tim nema potrebe. Trudite se biti inkluzivni!str.isdecimal
Je li znak znamenka u dekadskom sustavu. Opet, za ASCII-znamenke možete pitati
znak in string.digits
.
Mnoge lex
ove metode primaju argument imena uvjet
. On može biti pojedini znak koji se traži, metoda oblika str.is...
poput ovih upravo navedenih, bilo koja funkcija (možete je i sami napisati, bilo preko def
ili lambda
) koja prima znak i vraća bool
; ili pak skup takvih, interpretiran kao disjunkcija. Recimo, {str.islower, str.isdecimal, '_', '$'}
znači „ili malo slovo, ili dekadska znamenka, ili donja crta, ili znak dolara”.
Metode lexera¶
Raznim metodama možemo unutar jednog prolaza pročitati i više znakova. Ovdje su neke.
lex.zvijezda(uvjet)
, skraćenolex * uvjet
čita nula ili više znakova (od trenutne pozicije) koji ispunjavaju uvjet
lex.plus(uvjet)
, skraćenolex + uvjet
čita jedan ili više znakova (od trenutne pozicije) koji ispunjavaju uvjet; prijavljuje grešku ako takvih nema
lex.pročitaj_do(uvjet, *, uključivo=True, više_redova=False)
, skraćenolex - uvjet
ililex <= uvjet
, ili paklex < uvjet
zauključivo=False
čita znakove dok ne naiđe na prvi znak koji ispunjava uvjet, prijavljuje grešku ako takvog nema;
uključivo
kazuje hoće li pročitati i taj znak, aviše_redova
hoće li tražiti znak i u kasnijim recima a ne samo u trenutnomlex.prirodni_broj(znak, nula=True)
čita prirodni broj (niz znamenaka bez vodećih nulâ) s početkom
znak
(najčešće znamo da dolazi prirodni broj tek kad vidimo prvu njegovu znamenku, ali ne uvijek — tada možemo za prvi argument staviti''
);nula
kazuje dozvoljavamo li čitanje0
kao prirodnog broja
Također, razne metode su nam na raspolaganju ako želimo pročitati samo jedan znak.
lex.čitaj()
, skraćenonext(lex)
čita i vraća sljedeći znak bez ikakve provjere; na kraju ulaza vraća
''
lex.vidi(uvjet)
, skraćenolex > uvjet
ispituje ispunjava li znak, koji bi sljedeći bio pročitan, uvjet
lex.nužno(uvjet)
, skraćenolex >> uvjet
čita sljedeći znak ako ispunjava uvjet, inače prijavljuje grešku
lex.slijedi(uvjet)
, skraćenolex >= uvjet
čita sljedeći znak ako i samo ako ispunjava uvjet; pokrata za
if lex > uvjet: lex >> uvjet
.
Kad zaključimo da smo pročitali dovoljno znakova (što smo pročitali od zadnjeg stvorenog tokena možemo vidjeti u lex.sadržaj
), vrijeme je da od njih konstruiramo neki token. Na raspolaganju nam je nekoliko metoda.
yield lex.token(T.TIP)
stvara i šalje dalje token tipa
T.TIP
i sadržajalex.sadržaj
yield lex.literal(T, *, case=True)
stvara i šalje dalje literal onog tipa koji ima odgovarajući (pročitani) sadržaj; recimo ako je
lex.sadržaj == '->'
, uz gore definiraniT
, to bi odaslaloToken(T.STRELICA, '->')
(skraćenoT.STRELICA'->'
); ako takvog nema prijavljuje grešku;case
govori traži li sadržaj uzimajući u obzir razliku velikih i malih slovayield lex.literal_ili(T.DEFAULT)
kao
lex.literal
, osim što ako takvog literala nema, vraća token tipaT.DEFAULT
lex.zanemari()
resetira
lex.sadržaj
; možemo zamisliti da konstruira neki token, i uništi ga umjesto da ga pošalje dalje; česta linija u lexeru jeif znak.isspace(): lex.zanemari()
, čime zanemarujemo bjeline u izvornom kodu (ali nam i dalje služe za razdvajanje tokena).
Ako želimo sami prijaviti grešku, to možemo učiniti pomoću raise lex.greška(poruka)
(ne moramo navesti poruku ako vepar ima dovoljno podataka za konstrukciju dovoljno dobre poruke).
Primjer¶
Jednom kad smo napisali lexer i dekorirali ga s @lexer
, možemo ga pozvati s nekim stringom da vidimo kako funkcionira i eventualno ispravimo greške. Evo jednog primjera s obzirom na gornji T
:
@lexer
def moj(lex):
for znak in lex:
if znak == '-':
if lex >= '>': yield lex.token(T.STRELICA)
else: yield lex.token(T.MINUS)
elif znak == '!':
lex >> '='
yield lex.token(T.RAZLIČITO)
elif znak.isdecimal():
lex.prirodni_broj(znak, nula=False)
yield lex.token(T.BROJ)
else: yield lex.literal(T)
>>> moj('-+->ii!=234/')
Tokenizacija: -+->ii!=234/
Znak #1 : MINUS'-'
Znak #2 : PLUS'+'
Znakovi #3–#4 : STRELICA'->'
Znak #5 : I'i'
Znak #6 : I'i'
Znakovi #7–#8 : RAZLIČITO'!='
Znakovi #9–#11 : BROJ'234'
Znak #12 : KROZ'/'