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žajem 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 580, 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 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
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.PLUSima 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.PUTAima podrazumijevani sadržaj'*', a token tipaT.RAZLIČITOima 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.BROJnema 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.Iponaša kao da je definiran sI = 'i', a u semantičkoj analizi za bilo koji tokenttipaT.Imož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 lexa — 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.isspaceJe li znak bjelina, poput razmaka, tabulatora, prelaska u novi red i sličnog.
str.isalphaJe 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.isdecimalJe li znak znamenka u dekadskom sustavu. Opet, za ASCII-znamenke možete pitati
znak in string.digits.
Mnoge lexove 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 - uvjetililex <= uvjet, ili paklex < uvjetzauključivo=Falsečita znakove dok ne naiđe na prvi znak koji ispunjava uvjet, prijavljuje grešku ako takvog nema;
uključivokazuje hoće li pročitati i taj znak, aviše_redovahoć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'');nulakazuje dozvoljavamo li čitanje0kao 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 > uvjetispituje 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 za tu svrhu.
yield lex.token(T.TIP)stvara i šalje dalje token tipa
T.TIPi sadržajalex.sadržajyield 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;casegovori 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.DEFAULTlex.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'/'