Analiza leksikore. Analiza leksikore e një fjale - çfarë është ajo? Shembuj

Programi para së gjithash ndahet në një sekuencë rreshtash, ose, siç thonë ata, leksemë. Bashkësia e leksemave ndahet në nënbashkësi të shkëputura (klasa leksikore). Shenjat hyjnë në të njëjtën klasë leksikore nëse nuk dallohen nga pikëpamja analizë Tora. Për shembull, gjatë analizimit, të gjithë identifikuesit mund të konsiderohen të njëjtë.

Madhësitë e klasave leksikore ndryshojnë. Për shembull, klasa leksikore e identifikuesve është, në përgjithësi, e pafundme. Nga ana tjetër, ka klasa leksikore që përbëhen nga vetëm një shenjë, për shembull, një nëngrup që përbëhet nga shenja if. Shumica e gjuhëve të programimit kanë klasat e mëposhtme leksikore: fjalë kyçe (një për secilën fjalë kyçe), identifikues, literale vargjesh, konstante numerike. Çdo nëngrup shoqërohet me një numër të quajtur identifikues i klasës leksikore (token) ose shkurtimisht, klasë leksikore.

Shembull. Konsideroni operatorin e gjuhës Pascal const pi = 3.1416;

  • Ky operator përbëhet nga shenjat e mëposhtme:
  • const - klasa leksikore Const_LC
  • pi - klasa leksikore Identifier_LC
  • = - klasa leksikore Relation_LC
  • 3.1416 - klasa leksikore Numri_LC

;

- klasa leksikore Pikëpresje_LC Analiza leksikore e gjuhëve të ndryshme programuese Disa gjuhë kanë veçori që e bëjnë të vështirë analiza leksikore. Gjuhët si Fortran dhe Cobol kërkojnë që konstruktet gjuhësore të vendosen në pozicione fikse në vargun e hyrjes. Ky rregullim i shenjave mund të jetë shumë i rëndësishëm në përcaktimin e korrektësisë së programit. Për shembull, kur prishni një linjë në Cobol, duhet të vendosni karakter të veçantë në kolonën e 6-të, përndryshe rreshti tjetër do të analizohet gabimisht. Trendi kryesor

gjuhët moderne programimi është vendosja e lirë e tekstit të programit., duke ilustruar vështirësinë e mundshme të njohjes së shenjave në Fortran. Në pohimin DO 5 I = 1,25 nuk mund të përcaktojmë se DO nuk është fjalë kyçe derisa të takojmë pikën dhjetore.

Nga ana tjetër, në deklaratën DO 5 I = 1.25, kemi shtatë shenja: fjalen DO, etiketën 5, identifikuesin I, operator =, konstante 1, presje dhe konstante 25. Për më tepër, derisa të hasim një presje, nuk mund të jemi të sigurt se DO është

Kur analizoni një deklaratë të tillë, duhet të kaloni vazhdimisht nga modaliteti "THEN, ELSE as keywords" në trajtimin "THEN, ELSE as identifiers" dhe mbrapa.

Në kapitullin e fundit, ju lashë një përpilues që pothuajse duhet të funksionojë, përveç se ne jemi ende të kufizuar në shenja me një karakter. Qëllimi i këtij tutoriali është të heqësh qafe këtë kufizim një herë e përgjithmonë. Kjo do të thotë se duhet të merremi me konceptin e një analizuesi (skaner) leksikor.

Ndoshta duhet të përmend pse ne kemi nevojë për një analizues leksikor në radhë të parë... në fund të fundit, deri më tani ne kemi qenë në gjendje t'ia dalim mirë pa një të tillë, edhe kur kemi ofruar shenja me shumë karaktere.

Arsyeja e vetme ka të bëjë me fjalë kyçe. Është një fakt i jetës kompjuterike që sintaksa e një fjale kyçe ka të njëjtën formë si sintaksa e çdo identifikuesi tjetër. Nuk mund të themi derisa të marrim fjalën e plotë nëse kjo është në të vërtetë një fjalë kyçe. Për shembull, ndryshorja IFILE dhe fjala kyçe IF duken njësoj derisa të arrini te karakteri i tretë. Në shembujt e deritanishëm ne kemi qenë gjithmonë në gjendje të marrim një vendim bazuar në karakterin e parë të tokenit, por kjo nuk është më e mundur kur janë të pranishme fjalët kyçe. Ne duhet të dimë se një varg i caktuar është një fjalë kyçe përpara se të fillojmë ta përpunojmë atë. Dhe kjo është arsyeja pse ne kemi nevojë për një skaner.

Në mësimin e fundit unë gjithashtu premtova se ne mund të siguronim shenja normale pa ndryshime të mëdha në atë që kemi bërë tashmë. Nuk gënjeva...mundemi, siç do ta shihni më vonë. Por sa herë që vendosa t'i fusja këto elemente në analizuesin që kishim ndërtuar tashmë, kisha ndjenja të këqija për ta. E gjithë kjo dukej shumë si një masë e përkohshme. Më në fund kuptova shkakun e problemit: instalova programin e analizës leksikore pa ju sqaruar më parë gjithçka rreth analizës leksikore dhe cilat janë alternativat. Deri tani kam shmangur me kujdes të ju jap shumë teori dhe, natyrisht, opsione alternative. Zakonisht nuk i marr mirë tekstet shkollore që japin njëzet e pesë mënyra të ndryshme për të bërë diçka, por nuk ka informacion se cila mënyrë funksionon më mirë për ju. Unë u përpoqa ta shmang këtë grackë duke ju treguar thjesht një mënyrë që funksionon.

Por kjo është një fushë e rëndësishme. Megjithëse analizuesi leksikor nuk është pothuajse pjesa më emocionuese e përpiluesit, ai shpesh ka ndikimin më të thellë në përvojën e përgjithshme të gjuhës pasi është pjesa që është më afër përdoruesit. Unë dola me një strukturë specifike skaner për t'u përdorur me KISS. Përputhet me përvojën që dua nga kjo gjuhë. Por mund të mos funksionojë fare për gjuhën që keni krijuar, kështu që në këtë rast mendoj se është e rëndësishme që ju të dini opsionet tuaja.

Kështu që unë do të devijoj përsëri nga rutina ime normale. Në këtë mësim do të shkojmë shumë më thellë se zakonisht në teorinë bazë të gjuhëve dhe gramatikave. Do të flas edhe për fusha të tjera përveç hartuesve në të cilat luan analiza leksikore rol të rëndësishëm. Së fundi, do t'ju tregoj disa alternativa për strukturën e analizuesit leksikor. Atëherë dhe vetëm atëherë do të kthehemi te analizuesi ynë nga kapitulli i fundit. Jini të durueshëm... Unë mendoj se do ta gjeni të vlefshme për të pritur. Në fakt, meqenëse skanerët kanë shumë përdorime jashtë kompajlerëve, do ta gjeni lehtësisht si mësimin më të dobishëm për ju.

Analiza leksikore

Analiza leksikore është procesi i skanimit të një rryme karakteresh hyrëse dhe ndarjes së tij në vargje të quajtura token. Shumica e librave mbi përpiluesit fillojnë këtu dhe i kushtojnë disa kapituj diskutimit të metodave të ndryshme për ndërtimin e skanerëve. Kjo qasje ka vendin e vet, por siç e keni parë tashmë, ka shumë gjëra që mund të bëni pa pasur nevojë ta trajtoni këtë çështje dhe, në fakt, skaneri që përfundojmë këtu nuk do të ngjajë vërtet me atë që përshkruajnë këto tekste . Shkak? Teoria e përpiluesve, dhe për këtë arsye programet që rrjedhin prej saj, duhet të funksionojnë me shumicën e rregullave të përgjithshme të analizimit. Ne nuk e bëjmë këtë. NË botën realeështë e mundur të përcaktohet sintaksa e një gjuhe në atë mënyrë që të mjaftojë një skaner mjaft i thjeshtë. Dhe si gjithmonë, KISS është motoja jonë.

Në mënyrë tipike, analizuesi leksikor ndërtohet si një pjesë e veçantë e përpiluesit, në mënyrë që analizuesi të shikojë në thelb vetëm rrymën e shenjave hyrëse. Në teori, nuk ka nevojë të veçohet ky funksion nga pjesa tjetër e analizuesit. Ekziston vetëm një grup ekuacionesh sintaksore që përcakton të gjithë gjuhën, kështu që në teori mund të shkruajmë të gjithë analizuesin në një modul.

Pse është e nevojshme ndarja? Përgjigja ka bazë teorike dhe praktike.

Në vitin 1956, Noam Chomsky përcaktoi "Hierarkinë Chomsky" për gramatikat. Këtu janë ata:

Lloji 0. I pakufizuar (për shembull anglisht)

Lloji 1. Kontekst i ndjeshëm

Lloji 2. Pa kontekst

Lloji 3. I rregullt.

Disa karakteristika gjuhët tipike programimi (sidomos ato më të vjetrat si Fortran) i klasifikojnë ato si Tipi 1, por shumica të gjitha gjuhët moderne të programimit mund të përshkruhen duke përdorur vetëm dy llojet e fundit, dhe ne do të punojmë me to këtu.

E mira e këtyre dy llojeve është se ka mënyra shumë specifike për t'i analizuar ato. Është treguar se çdo gramatikë e rregullt mund të analizohet duke përdorur një formë të veçantë të makinës abstrakte të quajtur makinë e gjendjes së fundme. Ne kemi implementuar tashmë makina me gjendje të fundme në disa nga programet tona të njohjes.

Në mënyrë të ngjashme, gramatikat e tipit 2 (pa kontekst) mund të analizohen gjithmonë duke përdorur një automat të stivës (makinë shtetërore e shtuar me një pirg). Kemi shitur edhe këto makina. Në vend që të zbatonim një pirg të qartë për të bërë punën, ne u mbështetëm në pirgun e integruar të lidhur me kodimin rekurziv, dhe kjo është në fakt metoda e preferuar për analizimin nga lart-poshtë.

Ndodh që në gramatikat reale praktike, pjesët që kualifikohen si shprehje të rregullta priren të jenë pjesë të nivelit të ulët, si përkufizimi i një identifikuesi:

::= [ | ]*

Siç kërkohet lloje të ndryshme makina abstrakte për analizimin e këtyre dy llojeve të gramatikave, ka kuptim që këto funksione të nivelit të ulët të ndahen në një modul të veçantë, një analizues leksikor, i cili është ndërtuar mbi idenë e një makine të gjendjes së fundme. Ideja është që të përdoret metoda më e thjeshtë e analizës e nevojshme për të përfunduar punën.

Ekziston një arsye tjetër, më praktike për ndarjen e skanerit nga analizuesi. Ne duam të mendojmë për skedarin e burimit të hyrjes si një rrjedhë karakteresh që i përpunojmë nga e djathta në të majtë pa kthim prapa. Në praktikë kjo është e pamundur. Pothuajse çdo gjuhë ka disa fjalë kyçe si IF, WHILE dhe FUND. Siç e përmenda më herët, ne në fakt nuk mund ta dimë nëse një varg i caktuar është një fjalë kyçe derisa të arrijmë në fund të tij, i cili përcaktohet nga një hapësirë ​​ose një kufizues tjetër. Pra, duhet ta ruajmë vargun mjaftueshëm për të zbuluar nëse kemi një fjalë kyçe apo jo. Kjo është një formë e kufizuar e kthimit prapa.

Prandaj, dizajni i një përpiluesi standard përfshin një ndarje të funksioneve të analizimit të nivelit të ulët dhe të nivelit të lartë. Një analizues leksikor punon në nivelin e karaktereve, duke mbledhur karaktere në vargje, etj., dhe duke ia kaluar ato analizuesit si shenja të pandashme. Është gjithashtu normale të lini skanerin të bëjë punën e identifikimit të fjalëve kyçe.

Makinat shtetërore dhe alternativat

Unë përmenda se shprehjet e rregullta mund të analizohen duke përdorur një makinë shtetërore. Në shumicën e librave mbi përpiluesit dhe gjithashtu në shumicën e përpiluesve, do ta gjeni të zbatuar fjalë për fjalë. Ata zakonisht kanë një zbatim të makinës reale të gjendjes me numra të plotë të përdorur për të përcaktuar gjendjen aktuale dhe një tabelë veprimesh të kryera për çdo kombinim të gjendjes aktuale dhe simbolit të hyrjes. Nëse shkruani një përpilues "front fund" duke përdorur mjetet popullore Unix LEX dhe YACC, kjo është ajo që do të merrni. Prodhimi i LEX është një makineri gjendjesh e zbatuar në C plus një tabelë veprimi që korrespondon me gramatikën hyrëse të LEX-it të dhënë. Prodhimi i YACC është i ngjashëm... një analizues artificial i drejtuar nga tabela plus një tabelë që korrespondon me sintaksën e gjuhës.

Megjithatë, ky nuk është opsioni i vetëm. Në kapitujt tanë të mëparshëm, keni parë shumë herë se është e mundur të zbatohen analizuesit në mënyrë specifike pa u marrë me tabela, rafte dhe variabla të gjendjes. Në fakt, në kapitullin 5, ju paralajmërova se nëse mendoni se keni nevojë për këto gjëra, mund të jeni duke bërë diçka të gabuar dhe nuk po përfitoni nga aftësitë e Pascal-it. Në thelb ekzistojnë dy mënyra për të përcaktuar gjendjen e një makine shtetërore: në mënyrë eksplicite, me një numër ose kod shtetëror, dhe në mënyrë të nënkuptuar, thjesht bazuar në faktin se jam në një vend specifik në kod (nëse është e martë, atëherë duhet të jetë Belgjika). Më parë ne mbështeteshim kryesisht në metoda të nënkuptuara, dhe mendoj se do të bini dakord që ato funksionojnë mirë këtu.

Në praktikë, mund të mos jetë as e nevojshme të kemi një analizues leksikor të mirëpërcaktuar. Kjo nuk është përvoja jonë e parë duke punuar me shenja me shumë karaktere. Në kapitullin 3, ne zgjeruam analizuesin tonë për t'i mbështetur ato dhe nuk kishim nevojë as për një analizues leksikor. Arsyeja ishte se, në një kontekst të ngushtë, ne gjithmonë mund të kuptonim thjesht duke parë një simbol të vetëm parashikues nëse kishim të bënim me një shifër, një ndryshore ose një operator. Në realitet, ne ndërtuam një analizues leksikor të shpërndarë duke përdorur procedurat GetName dhe GetNum.

Duke pasur parasysh fjalët kyçe, ne nuk mund të dimë më se me çfarë kemi të bëjmë derisa të lexohet i gjithë token. Kjo na çon në një skaner më të lokalizuar, megjithëse siç do ta shihni, ideja e një skaneri të shpërndarë ka ende meritat e saj.

Eksperimentet e skanimit

Përpara se të kthehemi te përpiluesi ynë, mund të jetë e dobishme të eksperimentojmë pak me konceptet e përgjithshme.

Le të fillojmë me dy përkufizimet që gjenden më shpesh në gjuhët reale të programimit:

::= [ | ]*

]+

(Mos harroni se "*" tregon zero ose më shumë përsëritje të gjendjes në kllapa dhe "+" tregon një ose më shumë.)

Ne kemi punuar tashmë me elementë të ngjashëm në kapitullin e tretë. Le të fillojmë (si zakonisht) me një Djep bosh. Jo çuditërisht, do të na duhet një procedurë e re njohjeje:

Duke e përdorur atë, le të shkruajmë dy rutinat e mëposhtme, të cilat janë shumë të ngjashme me ato që kemi përdorur më parë:

(Merr një identifikues)

funksioni GetName: varg;

ndërsa IsAlNum(Shiko) fillon

x:= x + Rasti i madh (Shiko);

(Merr një numër)

funksioni GetNum: varg;

ndërsa IsDigit (Shiko) fillon

(Vini re se ky version i GetNum kthen një varg, jo një numër të plotë si më parë.)

Mund të kontrolloni lehtësisht nëse këto rutina funksionojnë duke i thirrur nga programi kryesor:

WriteLn(GetName);

Ky program do të nxjerrë çdo emër të vlefshëm që keni shtypur (maksimumi tetë karaktere, sepse ne i thamë GetName ta bënte këtë). Ajo do të refuzojë çdo gjë tjetër.

Kontrolloni një nënprogram tjetër në të njëjtën mënyrë.

Hapësirë

Më parë, ne kemi punuar gjithashtu me hapësira të mbivendosura duke përdorur dy rutina, IsWhite dhe SkipWhite. Sigurohuni që këto rutina janë në versionin tuaj aktual të Cradle dhe shtoni rreshtin:

në fund GetName dhe GetNum.

Tani le të përcaktojmë një procedurë të re:

(Skaner leksikor)

Skanimi i funksionit: varg;

nëse IsAlpha (Shiko) atëherë

tjetër nëse IsDigit(Shiko) atëherë

Mund ta quajmë nga programi i ri kryesor:

(Programi kryesor)

derisa Token = CR;

(Duhet të shtoni një përshkrim të vargut Token në fillim të programit. Bëjeni atë sipas gjatësisë që dëshironi, le të themi 16 karaktere.)

Tani ekzekutoni programin. Vini re se vargu i hyrjes është me të vërtetë i ndarë në shenja individuale.

Makinat e gjendjes së fundme

Rutina e analizës së llojit GetName zbaton në fakt një makinë shtetërore. Gjendja është e nënkuptuar në pozicionin aktual në kod. Një teknikë shumë e dobishme për vizualizimin e asaj që po ndodh është një diagram sintaksor ose diagrami "railroad-track". Është pak e vështirë t'i vizatosh në këtë medium, ndaj do t'i përdor me shumë kursim, por figura më poshtë duhet t'ju japë një ide:

Siç mund ta shihni, ky diagram tregon rrjedhat logjike ndërsa lexohen karakteret. Gjithçka fillon, natyrisht, me gjendjen "fillimi" dhe përfundon kur gjendet një karakter i ndryshëm nga ai alfanumerik. Nëse karakteri i parë nuk është një shkronjë, ndodh një gabim. Përndryshe, makina do të vazhdojë të ekzekutojë lak derisa të gjendet ndarësi fundor.

Vini re se në çdo pikë të rrjedhës pozicioni ynë varet tërësisht nga historia e mëparshme e simboleve hyrëse. Në këtë pikë, veprimet e ndërmarra varen vetëm nga gjendja aktuale plus simboli aktual i hyrjes. Kjo është ajo që përbën një makinë të gjendjes së fundme.

Për shkak të vështirësive të paraqitjes së diagrameve "railroad-shin" në këtë mjedis, unë do të vazhdoj t'i përmbahem ekuacioneve sintaksore tani e tutje. Por unë rekomandoj shumë diagrame për çdo gjë që përfshin analizimin. Me pak praktikë, mund të filloni të shihni se si të shkruani një analizues direkt nga diagrami. Shtigjet paralele janë të koduara në veprimet e kontrollit (duke përdorur deklaratat IF ose CASE), shtigjet sekuenciale në thirrjet sekuenciale. Është pothuajse si të punosh sipas një modeli.

Ne as që kemi diskutuar SkipWhite, i cili u prezantua më herët, por është gjithashtu një makinë e thjeshtë shtetërore si GetNum. Ashtu si procedura e tyre prindërore Scan. Automatet e vogla formojnë automata të mëdha.

Gjëja interesante që do të doja të vini re është se sa pa dhimbje kjo qasje e nënkuptuar i krijon këto makineri shtetërore. Unë personalisht e preferoj atë ndaj metodës së drejtuar nga tabela. Gjithashtu merr skanerë të vegjël, kompakt dhe të shpejtë.

Linja të reja

Duke ecur drejt përpara, le të modifikojmë skanerin tonë për të mbështetur më shumë se një linjë. Siç e përmenda herën e fundit, mënyra më e thjeshtë për ta bërë këtë është thjesht të trajtoni linjat e reja, kthimet e karrocave dhe furnizimet e linjave si hapësirë ​​të bardhë. Kjo është metoda e përdorur nga rutina iswhite në bibliotekën standarde C. Ne nuk e kemi bërë këtë më parë. Unë do të doja ta bëja këtë tani që të mund të ndjeni rezultatet.

Për ta bërë këtë thjesht ndryshoni linjën e vetme të ekzekutueshme në IsWhite:

Është e bardhë:= c në [" ", TAB, CR, LF];

Ne duhet t'i japim programit kryesor një kusht të ri ndalimi pasi ai nuk do ta shohë kurrë CR. Le të përdorim vetëm:

derisa Token = ".";

OK, përpiloni këtë program dhe ekzekutoni atë. Provoni disa rreshta që përfundojnë me pikë. Kam përdorur:

për të gjithë burrat e mirë.

Hej, çfarë ndodhi? Kur e shtypa këtë, nuk mora shenjën e fundit, pikën. Programi nuk ka të ndalur. Për më tepër, kur shtypa tastin "enter" disa herë, përsëri nuk më kapte pika.

Nëse ende nuk mund të dilni nga programi juaj, do të zbuloni se shtypja e një pikë në një rresht të ri do ta prishë atë.

Çfarë po ndodh këtu? Përgjigja është se ne rrimë në SkipWhite. Një vështrim i shpejtë në këtë rutinë do të tregojë se ndërkohë që po shtypim vija boshe, thjesht po vazhdojmë ciklin. Pasi SkipWhite takon LF, ai përpiqet të ekzekutojë GetChar. Por meqenëse buferi i hyrjes është tani bosh, operatori i leximit në GetChar këmbëngul që të ketë një varg tjetër. Procedura Scan merr një pikë pasuese, gjithçka është e saktë, por ajo thërret SkipWhite dhe SkipWhite nuk kthehet derisa të marrë një varg jo bosh.

Kjo sjellje nuk është aq e keqe sa duket. Në një përpilues të vërtetë, ne do të lexonim karaktere nga skedari i hyrjes në vend të tastierës, dhe për sa kohë që kemi një lloj procedure për të punuar me fundin e skedarit, gjithçka do të dalë në rregull. Por për leximin e të dhënave nga tastiera, kjo sjellje është shumë e zbukuruar. Në fund të fundit është se konventa C/Unix thjesht nuk është në përputhje me dizajnin e analizuesit tonë, i cili kërkon një karakter parashikues. Kodi që kanë zbatuar njerëzit e Bell nuk e përdor këtë konventë, kështu që ata kanë nevojë për "ungetc".

OK, le ta rregullojmë problemin. Për ta bërë këtë, ne duhet të kthehemi në përkufizimin e vjetër të IsWhite (heqim karakteret CR dhe LF) dhe të përdorim procedurën Fin që prezantova herën e kaluar. Nëse nuk është në versionin tuaj aktual të Cradle, vendoseni atje.

Ndryshoni gjithashtu programin kryesor si më poshtë:

(Programi kryesor)

nëse Token = CR atëherë Fin;

derisa Token = ".";

Vini re çekun "roje" që i paraprin thirrjes në Fin. Kjo është ajo që e bën gjithçka të funksionojë dhe kontrollon që ne të mos përpiqemi ta lexojmë rreshtin më tej.

Provoni këtë kod tani. Unë mendoj se do t'ju pëlqejë më shumë.

Nëse shikoni kodin që kemi shkruar në kapitullin e fundit, do të zbuloni se kam vendosur thirrje Fin në të gjithë kodin ku një ndërprerje e linjës do të ishte e përshtatshme. Kjo është një nga fushat që me të vërtetë ndikon në perceptimin që përmenda. Në këtë pikë unë duhet t'ju inkurajoj të eksperimentoni me mënyra të ndryshme organizimi dhe të shihni se si ju pëlqen. Nëse dëshironi që gjuha juaj të jetë vërtet e stilit të lirë, atëherë linjat e reja duhet të jenë transparente. Në këtë rast, qasja më e mirë do të ishte vendosja e rreshtave të mëposhtëm në fillim të Skanimit:

ndërsa Shiko = CR bëj

Nëse, nga ana tjetër, dëshironi një gjuhë të orientuar drejt linjës si Assembly, BASIC ose FORTRAN (ose edhe Ada... vini re se ka komente të përfunduara me rreshta të rinj), atëherë ju duhet Scan për të kthyer CR-të si shenja. Duhet gjithashtu të hajë LF-të përfundimtare. Mënyra më e mirë për ta bërë këtë është të përdorni këtë linjë që në fillim të Skanimit:

nëse Look = LF atëherë Fin;

Për marrëveshje të tjera, do t'ju duhet të përdorni metoda të tjera organizimi. Në shembullin tim në mësimin e fundit, unë lejova vetëm linja të reja në vende të caktuara, kështu që përfundova diku në mes. Në pjesën tjetër të këtyre mësimeve, unë do të zgjedh çfarëdo mënyrash që më pëlqen për të trajtuar linjat e reja, por dua që ju të dini se si të zgjidhni një rrugë tjetër për veten tuaj.

Operatorët

Mund të ndalemi tani dhe të kemi në dispozicion një skaner mjaft të dobishëm. Në fragmentet KISS që kemi ndërtuar, të vetmet shenja me shumë karaktere janë identifikuesit dhe numrat. Të gjitha deklaratat ishin me një karakter. I vetmi përjashtim që mund të mendoj janë operatorët relacionalë "<=», «>=" dhe "<>’, por ato mund të trajtohen si raste të veçanta.

Megjithatë, gjuhët e tjera kanë operatorë me shumë karaktere si ":=" në Pascal ose "++" dhe ">>" në C. Edhe pse nuk kemi nevojë për operatorë me shumë karaktere, do të ishte mirë të dinim si t'i merrni ato nëse është e nevojshme.

Është e vetëkuptueshme që ne mund t'i përpunojmë operatorët në të njëjtën mënyrë si shenjat e tjera. Le të fillojmë me rutinën e njohjes:

(Njoh çdo operator)

funksioni IsOp(c: char): boolean;

IsOp:= c në ["+", "-", "*", "/", "<", ">", ":", "="];

Është e rëndësishme të theksohet se ne nuk duhet të përfshijmë çdo operator të mundshëm në këtë listë. Për shembull, kllapat nuk përfshihen, siç është periudha pasuese. Versioni aktual i Scan tashmë mbështet mirë operatorët me një karakter. Lista e mësipërme përfshin vetëm ato karaktere që mund të shfaqen në deklaratat me shumë karaktere. (Natyrisht, lista mund të modifikohet gjithmonë për gjuhë të veçanta).

Tani le të ndryshojmë Skanimin si më poshtë:

(Skaner leksikor)

Skanimi i funksionit: varg;

ndërsa Shiko = CR bëj

nëse IsAlpha (Shiko) atëherë

tjetër nëse IsDigit(Shiko) atëherë

përndryshe nëse IsOp(Shiko) atëherë

Tani provoni programin. Do të siguroheni që çdo copë kodi që dëshironi të hidhni në të do të ndahet mjeshtërisht në shenja individuale.

Listat, presjet dhe linjat e komandës

Para se të kthehem te qëllimi kryesor i trajnimit tonë, do të doja të flisja pak.

Sa herë keni punuar me programin ose sistemi operativ e cila kishte rregulla strikte se si duhet t'i ndani artikujt në një listë? (Do të përpiqem, herën e fundit që keni përdorur MS DOS!). Disa programe kërkojnë hapësira si ndarës, disa kërkojnë presje. Pjesa më e keqe është se disa i kërkojnë të dyja në vende të ndryshme. Shumica janë mjaft të pafalshëm për shkeljet e rregullave të tyre.

Mendoj se kjo është e pafalshme. Është shumë e lehtë të shkruash një analizues që mbështet si hapësirat ashtu edhe presjet në një mënyrë fleksibël. Merrni parasysh procedurën e mëposhtme:

(Kalo mbi një presje)

procedura SkipComma;

nëse Shiko = "," atëherë filloni

Kjo procedurë me tetë rreshta do të kapërcejë një kufizues të përbërë nga çdo numër (përfshirë zero) hapësira, me zero ose një presje të vendosur brenda rreshtit.

Ndryshoni përkohësisht thirrjen SkipWhite në Scan në një telefonatë SkipComma dhe provoni të futni disa lista. Punon mirë, apo jo? A nuk dëshironi që më shumë krijues të softuerit të dinin për SkipComma?

Nga rruga, zbulova se shtimi i ekuivalentit SkipComma në programin tim të gjuhës së asamblesë Z80 mori vetëm gjashtë bajt shtesë kodi. Edhe në makinat 64K kjo nuk është shumë çmim i madh për të qenë miqësor me përdoruesit.

Unë mendoj se ju mund të shihni se ku po shkoj me këtë. Edhe nëse nuk keni shkruar kurrë një rresht të vetëm kodi përpiluesi në jetën tuaj, ka vende në çdo program ku mund të përdorni konceptin e analizimit. Çdo program që përpunon linjat e komandës ka nevojë për të. Në fakt, nëse e mendoni pak, do të arrini në përfundimin se sa herë që shkruani një program që përpunon hyrjen e përdoruesit, ju jeni duke përcaktuar një gjuhë. Njerëzit komunikojnë duke përdorur gjuhë dhe sintaksa e nënkuptuar në programin tuaj e përcakton atë gjuhë. Pyetja e vërtetë: do ta përkufizoni qëllimisht dhe në mënyrë eksplicite, apo thjesht do ta lejoni të ekzistojë pavarësisht se si programi përfundon analizën?

Unë argumentoj se do të keni një ndërfaqe më të mirë, më miqësore për përdoruesit, nëse merrni kohë për të përcaktuar sintaksën në mënyrë eksplicite. Shkruani ekuacionet sintaksore ose vizatoni diagramet "railroad-track" dhe kodoni analizuesin duke përdorur metodat që ju tregova këtu. Do të keni një program më të mirë dhe do të jetë më e lehtë të shkruani, të nisni.

Mirë, tani kemi një analizues mjaft të mirë leksikor që e thyen rrjedhën e hyrjes në shenja. Ne mund ta përdorim atë siç është dhe të kemi një përpilues të dobishëm. Por ka disa aspekte të tjera të analizës leksikore që duhet t'i mbulojmë.

Veçanërisht që ia vlen të merret në konsideratë është efikasiteti (dridhja). Mbani mend, kur punonim me shenja me një karakter, çdo test krahasonte një karakter të vetëm Look me një konstante bajt. Kryesisht kemi përdorur edhe operatorin Case.

Me shenjat me shumë karaktere të kthyera nga Scan, të gjitha këto kontrolle bëhen krahasime të vargjeve. Shumë më ngadalë. Dhe jo vetëm më i ngadalshëm, por edhe më i papërshtatshëm, pasi në Pascal nuk ka asnjë ekuivalent të vargut të operatorit Case. Duket veçanërisht e kotë të kontrollosh se çfarë përbëhet nga një karakter i vetëm... "=", "+" dhe operatorë të tjerë... duke përdorur krahasimin e vargjeve.

Krahasimi i vargjeve nuk është i pamundur. Ron Kane e përdori këtë qasje kur shkruante Small C. Meqenëse ne i përmbahemi parimit KISS, do të justifikoheshim të pajtoheshim me këtë qasje. Por atëherë nuk do të mund t'ju tregoja për njërën prej tyre metodat kryesore, përdoret në përpiluesit "real".

Duhet të mbani mend: analizuesi leksikor do të thirret shpesh! Në fakt, një herë për çdo shenjë në të gjithë programin burimor. Eksperimentet kanë treguar se përpiluesi mesatar shpenzon diku ndërmjet 20 dhe 40 për qind të kohës së tij në rutinat e analizës leksikore. Nëse ka pasur ndonjëherë një vend ku efikasiteti meriton një vështrim më të afërt, ky është ai.

Për këtë arsye, shumica e shkrimtarëve të përpiluesit e bëjnë analizuesin leksikor të bëjë pak më shumë punë duke "tokenizuar" rrjedhën hyrëse. Ideja është të krahasohet çdo shenjë me një listë fjalësh kyçe dhe operatorësh të vlefshëm dhe të kthehet një kod unik për secilën prej tyre të njohur. Në rastin e një emri ose numri të ndryshores së rregullt, ne thjesht kthejmë kodin që thotë se çfarë lloj token është dhe ruajmë vargun aktual diku.

Gjëja e parë që na nevojitet është një mënyrë për të identifikuar fjalët kyçe. Ne gjithmonë mund ta bëjmë këtë me kontrolle të njëpasnjëshme IF, por sigurisht që do të ishte mirë nëse do të kishim një rutinë të përgjithshme që mund të krahasonte një varg të caktuar me një tabelë me fjalë kyçe. (Meqë ra fjala, më vonë do të na duhet e njëjta nënrutinë për të punuar me tabelën identifikuese). Kjo zakonisht ekspozon problemin Pascal sepse standardi Pascal nuk ka vargje me gjatësi të ndryshueshme. Është një dhimbje e vërtetë të deklarosh rutina të ndryshme kërkimi për secilën tabelë. Standard Pascal gjithashtu nuk lejon që vargjet të inicializohen, kështu që do të duhet të shihni kodin si:

Tabela := "TJETER";

Tabela[n] := "FUND";

të cilat mund të zgjasin mjaft nëse ka shumë fjalë kyçe.

Për fat të mirë, Turbo Pascal 4.0 ka zgjerime që rregullojnë të dyja këto probleme. Vargjet konstante mund të deklarohen duke përdorur strukturën "konstante të shtypur" të TP dhe variablat e dimensioneve mund të mbështeten duke përdorur shtesa të ngjashme me C për treguesit.

Së pari, ndryshoni deklaratat tuaja si kjo:

(Deklaratat e tipit)

lloji Simbol = varg;

TabPtr = ^SymTab;

(Dimensioni i përdorur në SymTab nuk është real... kujtesa nuk ndahet drejtpërdrejt nga kjo deklaratë dhe dimensioni duhet vetëm të jetë "mjaft i madh")

Më pas, menjëherë pas këtyre deklaratave, shtoni sa vijon:

Më pas, ngjitni sa vijon veçori e re:

(Kërkimi i tabelës)

( Nëse vargu i hyrjes përputhet me një hyrje në tabelë, kthejeni hyrjen

indeks. Nëse jo, ktheni një zero. )

ndërsa (i > 0) dhe nuk u gjet do

nëse s = T^[i] atëherë

Për ta testuar atë, mund të ndryshoni përkohësisht programin kryesor si më poshtë:

(Programi kryesor)

WriteLn(Kërkimi (Addr(KWList), Token, 4));

Vini re se si quhet Lookup: funksioni Adr vendos një tregues në një KWList, i cili kalon në Lookup.

OK, provoje. Meqenëse këtu po anashkalojmë Skanimin, duhet të shkruani fjalët kyçe me shkronja të mëdha për të marrë një përputhje.

Tani që mund të njohim fjalë kyçe, hapi tjetër është të biem dakord për kodet e kthimit për to.

Pra, çfarë lloj kodi duhet të kthejmë? Në të vërtetë ekzistojnë vetëm dy opsione të zbatueshme. Ky duket si një aplikim ideal i tipit të numëruar të Pascal-it. Për shembull, ju mund të përcaktoni diçka të tillë

SymType = (IfSym, ElseSym, EndifSym, EndSym, Ident, Numri, Operatori);

dhe pranoni të ktheni një variabël të këtij lloji. Le ta provojmë këtë. Fusni rreshtin e mësipërm në përshkrimin e llojit.

Tani shtoni dy deklarata të variablave:

Token: Symtype; (Token aktual)

Vlera: String; (String Token of Look)

Ndryshoni skanerin si kjo:

(Skaner leksikor)

ndërsa Shiko = CR bëj

nëse IsAlpha (Shiko) atëherë filloni

Vlera:= GetEmri;

Token:= SymType(k – 1);

përndryshe nëse IsDigit(Shiko), atëherë filloni

përndryshe nëse IsOp (Shiko), atëherë filloni

Token:= Operatori;

Token:= Operatori;

(Vini re se Skanimi tani është një procedurë dhe jo një funksion.)

Së fundi, ndryshoni programin kryesor:

(Programi kryesor)

Identiteti: shkruani ("Ident");

Numri: Shkruani ("Numri");

Operatori: Write("Operator");

IfSym, ElseSym, EndifSym, EndSym: Shkruani("Fjala kyçe");

deri në Token = EndSym;

Ne kemi zëvendësuar vargun Token të përdorur më parë me një lloj enum. Scan kthen llojin në ndryshoren Token dhe kthen vetë vargun në një ndryshore të re Vlera.

OK, përpiloni programin dhe ekzekutoni atë. Nëse gjithçka funksionon, duhet të shihni që ne tani njohim fjalë kyçe.

Tani kemi gjithçka që funksionon si duhet dhe ishte e lehtë ta gjeneronim këtë nga ajo që kishim më parë. Megjithatë, ende më duket pak e "mbingarkuar". Ne mund ta thjeshtojmë këtë pak duke lejuar GetName, GetNum, GetOp dhe Scan të veprojnë në variablat globale Token dhe Value, duke fshirë kështu kopjet e tyre lokale. Duket pak më e zgjuar të zhvendosësh pamjen e tabelës në GetName. Pastaj formë e re për këto katër procedura do të jetë kështu:

(Merr një identifikues)

procedura GetName;

nëse jo IsAlpha (Shiko) atëherë Pritet ("Emri");

ndërsa IsAlNum(Shiko) fillon

Vlera:= Vlera + Rasti i madh (Shiko);

k:= Kërko (Addr(KWlist), Vlera, 4);

Token:= SymType(k-1);

(Merr një numër)

procedura GetNum;

nëse jo IsDigit(Look) atëherë Expected("Integer");

ndërsa IsDigit (Shiko) fillon

Vlera:= Vlera + Shiko;

(Merr një operator)

procedura GetOp;

ndërsa IsOp (Shiko) fillon

Vlera:= Vlera + Shiko;

Token:= Operatori;

(Skaner leksikor)

ndërsa Shiko = CR bëj

nëse IsAlpha (Shiko) atëherë

tjetër nëse IsDigit(Shiko) atëherë

përndryshe nëse IsOp(Shiko) atëherë

Token:= Operatori;

Kthimi i simbolit

Në thelb, çdo skaner që kam parë ndonjëherë që ishte shkruar në Pascal përdorte mekanizmin e tipit të numëruar që sapo përshkrova. Sigurisht që është një mekanizëm që funksionon, por nuk më duket si qasja më e thjeshtë.

Para së gjithash, lista llojet e mundshme personazhet mund të zgjasin mjaft. Këtu kam përdorur vetëm një karakter "Operator" për të përfaqësuar të gjithë operatorët, por kam parë projekte të tjera që në fakt kthejnë kode të ndryshme për secilin.

Sigurisht, ekziston një lloj tjetër i thjeshtë që mund të kthehet si kod: karakteri. Në vend që të ktheni vlerën "Operator" për shenjën "+", çfarë të keqe ka vetëm të ktheni vetë karakterin? Karakteri është gjithashtu një variabël i mirë për kodimin e llojeve të ndryshme të shenjave, ai mund të përdoret lehtësisht në deklaratat Case dhe është shumë më e lehtë për të shtypur. Çfarë mund të jetë më e thjeshtë?

Për më tepër, ne kemi pasur tashmë përvojë me idenë e kodimit të fjalëve kyçe si karaktere të vetme. Programet tona të mëparshme janë shkruar tashmë në këtë mënyrë, kështu që përdorimi i kësaj metode minimizon ndryshimet në atë që kemi bërë tashmë.

Disa prej jush mund të mendojnë se ideja e kthimit të kodeve të karaktereve është shumë fëminore. Më duhet të pranoj se bëhet pak e vështirë për operatorë si "<=». Если вы хотите остаться с перечислимыми типами, хорошо. Для остальных я хотел бы показать как изменить то, что мы сделали выше, для поддержки такого подхода.

Së pari, mund ta hiqni deklaratën SymType tani...nuk do të na duhet më. Dhe mund ta ndryshoni llojin Token në char.

Pastaj, për të zëvendësuar SymType, shtoni konstanten e mëposhtme:

(Unë do t'i kodoj të gjithë identifikuesit me një "x" të vetme).

Më në fund modifikoni Scan dhe të afërmit e tij si kjo:

(Merr një identifikues)

procedura GetName;

nëse jo IsAlpha (Shiko) atëherë Pritet ("Emri");

ndërsa IsAlNum(Shiko) fillon

Vlera:= Vlera + Rasti i madh (Shiko);

(Merr një numër)

procedura GetNum;

nëse jo IsDigit(Look) atëherë Expected("Integer");

ndërsa IsDigit (Shiko) fillon

Vlera:= Vlera + Shiko;

(Merr një operator)

procedura GetOp;

nëse jo IsOp(Look) atëherë Expected ("Operator");

ndërsa IsOp (Shiko) fillon

Vlera:= Vlera + Shiko;

nëse Gjatësia(Vlera) = 1 atëherë

(Skaner leksikor)

ndërsa Shiko = CR bëj

nëse IsAlpha (Shiko) atëherë

tjetër nëse IsDigit(Shiko) atëherë

përndryshe nëse IsOp (Shiko), atëherë filloni

(Programi kryesor)

"x": shkruani("Ident");

"#": Shkruani ("Numër");

"i", "l", "e": Shkruaj("Fjala kyçe");

other Write ("Operator");

deri në Vlera = "(! LANG: FUND";!}

Ky program duhet të funksionojë njësoj si versioni i mëparshëm. Ndoshta një ndryshim i vogël në strukturë, por mua më duket më e thjeshtë.

Të shpërndarë kundrejt skanerëve të centralizuar

Struktura e analizuesit leksikor që sapo ju tregova është mjaft standarde, dhe rreth 99% e të gjithë përpiluesve përdorin diçka shumë të afërt me të. Kjo, megjithatë, nuk është struktura e vetme e mundshme, apo edhe gjithmonë më e mira.

Problemi me qasjen standarde është se skaneri nuk ka njohuri për kontekstin. Për shembull, ai nuk mund të bëjë dallimin midis operatorit të caktimit "=" dhe operatorit relacional "=" (që ndoshta është arsyeja pse të dy C dhe Pascal përdorin vargje të ndryshme për ta). Gjithçka që skaneri mund të bëjë është t'ia kalojë deklaratën analizuesit, i cili mund të tregojë saktësisht nga konteksti se çfarë lloj deklarate është. Po kështu, fjala kyçe "IF" mund të mos jetë në mes të një shprehjeje aritmetike, por nëse ndodh aty, skaneri nuk do të shohë asnjë problem dhe do ta kthejë atë në analizues, të koduar saktë si "IF".

Me këtë qasje, ne në fakt nuk përdorim të gjithë informacionin që kemi në dispozicion. Në mes të një shprehjeje, për shembull, analizuesi "e di" se nuk ka nevojë të kërkojë një fjalë kyçe, por nuk ka asnjë mënyrë për t'ia treguar këtë skanerit. Kështu që skaneri vazhdon ta bëjë këtë. Kjo sigurisht ngadalëson përpilimin.

Në përpiluesit e vërtetë, dizajnerët shpesh marrin hapa për të kaluar informacion të detajuar midis skanerit dhe analizuesit vetëm për të shmangur këto lloj problemesh. Por kjo mund të jetë e ngathët dhe me siguri do të shkatërrojë një pjesë të modularitetit në dizajnin e përpiluesit.

Alternativa është të gjejmë një mënyrë për të përdorur informacionin e kontekstit që vjen nga njohja se ku jemi në analizues. Kjo na kthen te koncepti i një skaneri të shpërndarë, në të cilin thirren pjesë të ndryshme të skanerit në varësi të kontekstit.

Në KISS, si shumica e gjuhëve, fjalët kyçe shfaqen vetëm në fillim të një deklarate. Në vende të tilla si shprehje ato janë të ndaluara. Gjithashtu, me një përjashtim të vogël (operatorë relacionalë me shumë karaktere) që është i lehtë për t'u trajtuar, të gjithë operatorët janë me një karakter, që do të thotë se nuk kemi nevojë fare për GetOp.

Pra, rezulton se edhe me shenja me shumë karaktere, ne mund të përcaktojmë gjithmonë me saktësi llojin e shenjës bazuar në karakterin aktual parashikues, duke përjashtuar vetë fillimin e deklaratës.

Edhe në këtë pikë, lloji i vetëm i shenjës që mund të pranojmë është një identifikues. Duhet vetëm të përcaktojmë nëse ky identifikues është një fjalë kyçe apo ana e majtë e një operatori caktimi.

Pastaj ne përfundojmë që ende kemi nevojë vetëm për GetName dhe GetNum, të cilat përdoren në të njëjtën mënyrë që i kemi përdorur në kapitujt e mëparshëm.

Në fillim mund t'ju duket se ky është një hap prapa dhe një metodë mjaft primitive. Në fakt, ky është një përmirësim në skanerin klasik, pasi ne përdorim rutinat e skanimit vetëm aty ku janë vërtet të nevojshme. Në vendet ku fjalët kyçe nuk lejohen, ne nuk e ngadalësojmë përpilimin duke i kërkuar ato.

Kombinimi i një skaneri dhe analizuesi

Tani që kemi mbuluar të gjithë teorinë dhe aspektet e përgjithshme të analizës leksikore, më në fund jam gati të mbështes pretendimin tim se ne mund të akomodojmë shenja me shumë karaktere me ndryshime minimale në punën tonë të mëparshme. Për hir të shkurtësisë dhe thjeshtësisë, do të kufizohem në një nëngrup të asaj që bëmë më parë: do të lejoj vetëm një konstrukt kontrolli (IF) dhe asnjë shprehje boolean. Kjo është e mjaftueshme për të demonstruar analizën dhe fjalët kyçe dhe shprehjet. Zgjerimi në një grup të plotë konstruksionesh duhet të jetë mjaft i qartë nga ajo që kemi bërë deri më tani.

Të gjithë elementët e programit për analizimin e këtij nëngrupi duke përdorur shenja me një karakter ekzistojnë tashmë në programet tona të mëparshme. E ndërtova duke i kopjuar me kujdes këto skedarë, por nuk do të guxoja të përpiqesha t'ju drejtoja në këtë proces. Në vend të kësaj, për të shmangur konfuzionin, i gjithë programi tregohet më poshtë:

(Deklarata të vazhdueshme)

(Deklaratat e tipit)

lloji Simbol = varg;

SymTab = grup simbolesh;

TabPtr = ^SymTab;

(Deklarata të ndryshueshme)

procedura GetChar;

(Raporto një gabim)

procedurë Gabim(et: varg);

(Raportoni gabimin dhe ndaloni)

procedurë Abort(s: varg);

(Raportoni atë që pritej)

Aborti(s + "Pritet");

(Njoh një shifër dhjetore)

IsDigit:= c në ["0".."9"];

(Njohni një karakter alfanumerik)

funksioni IsAlNum(c: char): boolean;

IsAlNum:= IsAlpha(c) ose IsDigit(c);

(Njoh një shtesë)

IsAddop:= c në ["+", "-"];

(Njoh një Mulop)

IsMulop:= c në ["*", "/"];

(Njoh hapësirën e bardhë)

IsWhite:= c në [" ", TAB];

procedura SkipWhite;

ndërsa IsWhite(Shiko) bëjnë

Procedura Përputhje (x: char);

nëse Shikoni<>

nëse Look = CR atëherë GetChar;

nëse Look = LF atëherë GetChar;

(Merr një identifikues)

funksioni GetName: char;

ndërsa Shiko = CR bëj

nëse jo IsAlpha (Shiko) atëherë Pritet ("Emri");

Getname:= UpCase(Shiko);

(Merr një numër)

funksioni GetNum: char;

nëse jo IsDigit(Look) atëherë Expected("Integer");

(Krijoni një etiketë unike)

funksioni NewLabel: varg;

NewLabel:= "L" + S;

(Postoni një etiketë në dalje)

WriteLn(L, ":");

(Nxjerr një varg me skedë)

procedura Emit(s: varg);

procedura EmitLn(s: varg);

Identifikimi i procedurës;

nëse Shiko = "(" atëherë filloni

EmitLn("BSR" + Emri);

EmitLn("MOVE " + Emri + "(PC),D0");

Faktori i procedurës;

nëse Shiko = "(" atëherë filloni

ndryshe nëse IsAlpha(Shiko) atëherë

EmitLn("MOVE #" + GetNum + ",D0");

procedura SignedFactor;

nëse IsAddop (Shiko), atëherë filloni

EmitLn ("NEG D0");

procedura Multiply;

EmitLn("MULS (SP)+,D0");

procedura Ndarja;

EmitLn("MOVE (SP)+,D1");

EmitLn ("EXS.L D0");

EmitLn ("DIVS D1,D0");

procedura Term1;

ndërsa IsMulop (Shiko) fillon

EmitLn("MOVE D0,-(SP)");

procedura Afati i Parë;

EmitLn("ADD (SP)+,D0");

procedura Zbris;

EmitLn("SUB (SP)+,D0");

EmitLn ("NEG D0");

procedurë Shprehje;

ndërsa IsAddop (Shiko) fillon

EmitLn("MOVE D0,-(SP)");

(Ky version është një bedel)

Kushtet e procedurës;

EmitLn ("Kushti");

Blloku i procedurës;

var L1, L2: varg;

EmitLn("BEQ" + L1);

nëse Shiko = "l" atëherë filloni

EmitLn("BRA" + L2);

procedurë Caktimi;

EmitLn("MOVE D0,(A0)");

Blloku i procedurës;

ndërsa jo (Shiko në ["e", "l"]) do të fillojë

CR: ndërsa Shiko = CR bëj

tjetër Detyrë;

procedura DoProgram;

nëse Shikoni<>"e" pastaj Pritet ("FUND");

(Programi kryesor)

Nja dy komente:

Forma e analizës së shprehjes duke përdorur FirstTerm dhe të ngjashme është paksa e ndryshme nga ajo që keni parë më parë. Ky është një variant tjetër në të njëjtën temë. Mos lejoni që ata t'ju lëkundin...ndryshimi nuk është i nevojshëm për atë që vjen më pas.

Përpara se të filloni të shtoni një skaner, fillimisht kopjoni këtë skedar dhe kontrolloni që në të vërtetë e kryen analizën saktë. Mos harroni "kodin": "i" për IF, "l" për ELSE dhe "e" për ELSE ose ENDIF.

Nëse programi funksionon, atëherë le të nxitojmë. Një plan sistematik do të ndihmojë kur shtohen modulet e skanerit në program. Në të gjithë analizuesit që kemi shkruar deri më tani, ne kemi ndjekur konventën që karakteri aktual parashikues duhet të jetë gjithmonë një karakter jo-nul. Ne ngarkuam paraprakisht simbolin parashikues në Init dhe më pas e lamë "pompën të funksiononte". Për të lejuar që programi të funksionojë siç duhet me linjat e reja, duhet ta modifikojmë pak dhe ta trajtojmë karakterin e linjës së re si një shenjë të vlefshme.

Në versionin me shumë karaktere, rregulli është i ngjashëm: karakteri aktual parashikues duhet të mbetet gjithmonë në fillim të shenjës tjetër ose në një linjë të re.

Versioni me shumë karaktere është paraqitur më poshtë. Për ta marrë atë, bëra ndryshimet e mëposhtme:

Shtuar variabla Token dhe Vlera dhe përkufizimet e llojit të kërkuara nga Lookup.

Përkufizimi i shtuar i KWList dhe KWcode.

Kërkim i shtuar.

GetName dhe GetNum janë zëvendësuar me versionet e tyre me shumë karaktere. (Vini re se thirrja e kërkimit është zhvendosur nga GetName, kështu që nuk do të ekzekutohet brenda shprehjeve).

Është krijuar një Skan i ri, rudimentar që thërret GetName dhe më pas skanon për fjalë kyçe.

Është krijuar një procedurë e re MatchString që kërkon një fjalë kyçe specifike. Vini re se ndryshe nga Match, MatchString nuk lexon fjalën kyçe të radhës.

Ndryshoi Bllokun për të thirrur Skan.

Thirrjet Fin janë ndryshuar pak. Fin tani thirret nga GetName.

Programi i plotë:

(Deklarata të vazhdueshme)

(Deklaratat e tipit)

lloji Simbol = varg;

SymTab = grup simbolesh;

TabPtr = ^SymTab;

(Deklarata të ndryshueshme)

var Shiko: char; (Karakteri i shikimit)

Token: char; (Token i koduar)

Vlera: varg; (Token i pakoduar)

Lcount: numër i plotë; (Numëruesi i etiketave)

(Përkufizimi i fjalëve kyçe dhe llojeve të shenjave)

const KWlist: grupi i simboleve =

("NËSE", "TJETER", "ENDIF", "FUND");

const KWcode: string = "xilee";

(Lexo karakterin e ri nga transmetimi hyrës)

procedura GetChar;

(Raporto një gabim)

procedurë Gabim(et: varg);

WriteLn(^G, "Gabim: ", s, ".");

(Raportoni gabimin dhe ndaloni)

procedurë Abort(s: varg);

(Raportoni atë që pritej)

procedura e pritshme(et: varg);

Aborti(s + "Pritet");

(Njohni një karakter alfa)

funksioni IsAlpha(c: char): boolean;

IsAlpha:= Rasti i madh (c) në ["A".."Z"];

(Njoh një shifër dhjetore)

funksioni IsDigit(c: char): boolean;

IsDigit:= c në ["0".."9"];

(Njohni një karakter alfanumerik)

funksioni IsAlNum(c: char): boolean;

IsAlNum:= IsAlpha(c) ose IsDigit(c);

(Njoh një shtesë)

funksioni IsAddop(c: char): boolean;

IsAddop:= c në ["+", "-"];

(Njoh një Mulop)

funksioni IsMulop(c: char): boolean;

IsMulop:= c në ["*", "/"];

(Njoh hapësirën e bardhë)

funksioni IsWhite(c: char): boolean;

IsWhite:= c në [" ", TAB];

(Kalo mbi Hapësirën e Bardhë kryesore)

procedura SkipWhite;

ndërsa IsWhite(Shiko) bëjnë

(Përputhni një karakter specifik të hyrjes)

Procedura Përputhje (x: char);

nëse Shikoni<>x pastaj Pritet("""" + x + """");

nëse Look = CR atëherë GetChar;

nëse Look = LF atëherë GetChar;

(Kërkimi i tabelës)

Kërkimi i funksionit (T: TabPtr; s: varg; n: numër i plotë): numër i plotë;

ndërsa (i > 0) dhe nuk u gjet do

nëse s = T^[i] atëherë

(Merr një identifikues)

procedura GetName;

ndërsa Shiko = CR bëj

nëse jo IsAlpha (Shiko) atëherë Pritet ("Emri");

ndërsa IsAlNum(Shiko) fillon

Vlera:= Vlera + Rasti i madh (Shiko);

(Merr një numër)

procedura GetNum;

nëse jo IsDigit(Look) atëherë Expected("Integer");

ndërsa IsDigit (Shiko) fillon

Vlera:= Vlera + Shiko;

(Merrni një identifikues dhe skanoni atë për fjalë kyçe)

Token:= KWcode;

(Përputhni një varg specifik të hyrjes)

procedura MatchString(x: varg);

nëse Vlera<>x pastaj Pritet("""" + x + """");

(Krijoni një etiketë unike)

funksioni NewLabel: varg;

NewLabel:= "L" + S;

(Postoni një etiketë në dalje)

procedura PostLabel(L: varg);

WriteLn(L, ":");

(Nxjerr një varg me skedë)

procedura Emit(s: varg);

(Nxirrni një varg me Tab dhe CRLF)

procedura EmitLn(s: varg);

(Panse dhe përkthe një identifikues)

Identifikimi i procedurës;

nëse Shiko = "(" atëherë filloni

EmitLn("BSR" + Vlera);

EmitLn("MOVE " + Vlera + "(PC),D0");

(Të analizojë dhe të përkthejë një faktor matematikor)

procedurë Shprehje; Përpara;

Faktori i procedurës;

nëse Shiko = "(" atëherë filloni

ndryshe nëse IsAlpha(Shiko) atëherë

EmitLn("MOVE #" + Vlera + ",D0");

(Panse dhe përkthe faktorin e parë matematikor)

procedura SignedFactor;

nëse IsAddop (Shiko), atëherë filloni

EmitLn ("NEG D0");

(Njohni dhe përktheni një shumëzim)

procedura Multiply;

EmitLn("MULS (SP)+,D0");

(Njohni dhe përktheni një ndarje)

procedura Ndarja;

EmitLn("MOVE (SP)+,D1");

EmitLn ("EXS.L D0");

EmitLn ("DIVS D1,D0");

(Përfundimi i përpunimit të afatit (i quajtur sipas afatit dhe afatit të parë)

procedura Term1;

ndërsa IsMulop (Shiko) fillon

EmitLn("MOVE D0,-(SP)");

(Anzoni dhe përktheni një term matematikor)

(Të analizojë dhe të përkthejë një term matematikor me shenjën e mundshme kryesore)

procedura Afati i Parë;

(Njohni dhe përktheni një Shtim)

EmitLn("ADD (SP)+,D0");

(Njohni dhe përktheni një zbritje)

procedura Zbris;

EmitLn("SUB (SP)+,D0");

EmitLn ("NEG D0");

(Panse dhe përkthe një shprehje)

procedurë Shprehje;

ndërsa IsAddop (Shiko) fillon

EmitLn("MOVE D0,-(SP)");

(Palzoni dhe përktheni një kusht Boolean)

(Ky version është një bedel)

Kushtet e procedurës;

EmitLn ("Kushti");

(Njohni dhe përktheni një konstruksion IF)

Blloku i procedurës; Përpara;

var L1, L2: varg;

EmitLn("BEQ" + L1);

nëse Token = "l" atëherë filloni

EmitLn("BRA" + L2);

MatchString ("ENDIF");

(Anzoni dhe përktheni një deklaratë të detyrës)

procedurë Caktimi;

var Emri: varg;

EmitLn("LEA " + Emri + "(PC),A0");

EmitLn("MOVE D0,(A0)");

(Njohni dhe përktheni një bllok deklaratash)

Blloku i procedurës;

ndërsa jo (Token në ["e", "l"]) do të fillojë

tjetër Detyrë;

(Të analizojë dhe të përkthejë një program)

procedura DoProgram;

MatchString ("FUND");

(Programi kryesor)

Krahasoni këtë program me versionin e tij me një karakter. Mendoj se do të pajtoheni që dallimet janë minimale.

konkluzioni

Deri tani, ju keni mësuar se si të analizoni dhe gjeneroni kodin për shprehjet, shprehjet Boolean dhe strukturat e kontrollit. Tani keni mësuar se si të zhvilloni analizues leksikor dhe si t'i ndërtoni elementet e tyre në përkthyes. Ende nuk i keni parë të gjithë elementët të kombinuar në një program, por bazuar në atë që bëmë më parë, duhet të arrini në përfundimin se është e lehtë të zgjerojmë programet tona të mëparshme për të përfshirë analizues leksikor.

Jemi shumë afër të kemi të gjithë elementët e nevojshëm për të ndërtuar një përpilues të vërtetë dhe funksional. Ka ende disa gjëra që mungojnë, veçanërisht thirrjet e procedurave dhe përkufizimet e tipit. Ne do të punojmë me ta gjatë mësimeve të ardhshme. Megjithatë, përpara se ta bëja këtë, mendova se do të ishte kënaqësi ta ktheja përkthyesin në një përpilues të vërtetë. Kjo është ajo që do të bëjmë në kapitullin tjetër.

Deri më tani ne kemi përdorur një metodë kryesisht nga poshtë-lart analizimi, duke filluar me konstruktet e nivelit të ulët dhe duke ecur lart. Në kapitullin tjetër do të hedh një vështrim nga lart-poshtë dhe do të diskutojmë se si ndryshon struktura e përkthyesit kur ndryshon përkufizimi i gjuhës.

Faqja 1 nga 14

HYRJE

Në kapitullin e fundit, ju lashë një përpilues që pothuajse duhet të funksionojë, përveç se ne jemi ende të kufizuar në shenja me një karakter. Qëllimi i këtij tutoriali është të heqësh qafe këtë kufizim një herë e përgjithmonë. Kjo do të thotë se duhet të merremi me konceptin e një analizuesi (skaner) leksikor.

Ndoshta duhet të përmend pse në radhë të parë na duhet një analizues leksikor... në fund të fundit, deri më tani ne kemi qenë në gjendje të bëjmë mirë pa një të tillë edhe kur kemi ofruar shenja me shumë karaktere.

Arsyeja e vetme ka të bëjë me fjalë kyçe. Është një fakt i jetës kompjuterike që sintaksa e një fjale kyçe ka të njëjtën formë si sintaksa e çdo identifikuesi tjetër. Nuk mund të themi derisa të marrim fjalën e plotë nëse kjo është në të vërtetë një fjalë kyçe. Për shembull, ndryshorja IFILE dhe fjala kyçe IF duken njësoj derisa të arrini te karakteri i tretë. Në shembujt e deritanishëm ne kemi qenë gjithmonë në gjendje të marrim një vendim bazuar në karakterin e parë të tokenit, por kjo nuk është më e mundur kur janë të pranishme fjalët kyçe. Ne duhet të dimë se një varg i caktuar është një fjalë kyçe përpara se të fillojmë ta përpunojmë atë. Dhe kjo është arsyeja pse ne kemi nevojë për një skaner.

Në mësimin e fundit unë gjithashtu premtova se ne mund të siguronim shenja normale pa ndryshime të mëdha në atë që kemi bërë tashmë. Nuk gënjeva...mundemi, siç do ta shihni më vonë. Por sa herë që vendosa t'i fusja këto elemente në analizuesin që kishim ndërtuar tashmë, kisha ndjenja të këqija për ta. E gjithë kjo dukej shumë si një masë e përkohshme. Më në fund kuptova shkakun e problemit: instalova programin e analizës leksikore pa ju sqaruar më parë gjithçka rreth analizës leksikore dhe cilat janë alternativat. Deri tani kam shmangur me kujdes të ju jap shumë teori dhe, natyrisht, opsione alternative. Zakonisht nuk i marr mirë tekstet shkollore që japin njëzet e pesë mënyra të ndryshme për të bërë diçka, por nuk ka informacion se cila mënyrë funksionon më mirë për ju. Unë u përpoqa ta shmang këtë grackë duke ju treguar thjesht një mënyrë që funksionon.

Por kjo është një fushë e rëndësishme. Megjithëse analizuesi leksikor nuk është pothuajse pjesa më emocionuese e përpiluesit, ai shpesh ka ndikimin më të thellë në përvojën e përgjithshme të gjuhës pasi është pjesa që është më afër përdoruesit. Unë dola me një strukturë specifike skaner për t'u përdorur me KISS. Përputhet me përvojën që dua nga kjo gjuhë. Por mund të mos funksionojë fare për gjuhën që keni krijuar, kështu që në këtë rast mendoj se është e rëndësishme që ju të dini opsionet tuaja.

Kështu që unë do të devijoj përsëri nga rutina ime normale. Në këtë mësim do të shkojmë shumë më thellë se zakonisht në teorinë bazë të gjuhëve dhe gramatikave. Do të flas edhe për fusha të tjera përveç hartuesve, në të cilat analiza leksikore luan një rol të rëndësishëm. Së fundi, do t'ju tregoj disa alternativa për strukturën e analizuesit leksikor. Atëherë dhe vetëm atëherë do të kthehemi te analizuesi ynë nga kapitulli i fundit. Jini të durueshëm... Unë mendoj se do ta gjeni të vlefshme për të pritur. Në fakt, meqenëse skanerët kanë shumë përdorime jashtë kompajlerëve, do ta gjeni lehtësisht si mësimin më të dobishëm për ju.

Në shkencat kompjuterike analiza leksikore - procesi i analizimit analitik të një sekuence hyrëse të karaktereve (për shembull, si kodi burim në një nga gjuhët e programimit) në mënyrë që të merret një sekuencë dalëse e karaktereve të quajtur leksema ose " argumentet"(e ngjashme me grupimin e shkronjave në fjalë). Kështu, në procesin e analizës leksikore, leksema njihen dhe veçohen nga sekuenca hyrëse e karaktereve.

Si rregull, analiza leksikore kryhet nga pikëpamja gjuhë specifike ose një grup gjuhësh. Një gjuhë, ose më mirë gramatika e saj, specifikon një grup të caktuar leksemash që mund të ndeshen në hyrje të një procesi.

Është tradicionale të organizohet procesi i analizës leksikore duke marrë parasysh sekuencën hyrëse të karaktereve si një rrjedhë karakteresh. Me këtë organizim, procesi kontrollon në mënyrë të pavarur përzgjedhjen e karaktereve individuale nga rrjedha e hyrjes.

Njohja e leksemave në kontekstin e një gramatike zakonisht bëhet duke i identifikuar (ose klasifikuar) ato sipas identifikuesve (ose klasave) të leksemave të përcaktuara nga gramatika e gjuhës. Në këtë rast, çdo sekuencë e karaktereve të rrjedhës hyrëse (token) që, sipas gramatikës, nuk mund të identifikohet si një shenjë gjuhësore, zakonisht konsiderohet një shenjë e veçantë gabimi.

Çdo leksemë mund të përfaqësohet si një strukturë që përmban identifikuesi i shenjës(ose identifikues i klasës së shenjës) dhe, nëse është e nevojshme, një sekuencë karakteresh leksema ekstraktuar nga rryma hyrëse (vargu, numri etj.).

Qëllimi i një konvertimi të tillë është zakonisht përgatitja e sekuencës hyrëse për një program tjetër, si p.sh. një analizues gramatikor, dhe për ta shpëtuar atë nga detyrimi për të përcaktuar detajet leksikore në një gramatikë pa kontekst (gjë që do ta bënte gramatikën më komplekse).

Analizë sintaksore (gramatikore).

Në shkencat kompjuterike analiza gramatikore (analizë, analizë) - është procesi i hartës së renditjes lineare të leksemave të një gjuhe me gramatikën e saj formale. Rezultati është zakonisht një pemë e analizuar. Zakonisht përdoret në lidhje me analizën leksikore, në procesin e analizës sintaksore.

Analizues gramatikor (analizues ) - një program ose algoritëm që kryen analizën gramatikore.

Proceset e përpilimit dhe interpretimit mund të përfaqësohen nga diagrami i mëposhtëm.

Përkthyes

Programi burimor

Interpretimi

Përpilues

Përmbledhje

Ekzekutimi

Programi i synuar

Oriz. 2.4. Përpilimi dhe interpretimi

Kompilime dhe interpretime të kombinuara

Strategjia e përzier supozon se përpiluesi krijon kodin në një gjuhë të ndërmjetme që është e kuptueshme për makinën virtuale

Programi burimor

Përpilues

Makinë virtuale

Përkthyes

Përmbledhje

Oriz. 2.5. Përmbledhje plus interpretim

Makinat virtuale, bytekodi dheJIT (VetëmKoha)

Zbatimi i gjuhëve të tilla si Java, C# dhe gjuhë të tjera të platformës .NET bazohet në një zgjidhje të përzier. Kodi i ndërmjetëm për Java quhet bytecode. Ky term pasqyron faktin që makina virtuale përdor instruksione kompakte të ngjashme me ato të procesorit aktual. Për të rritur efikasitetin e kohës së ekzekutimit të bytekodit, përdoren përpiluesit JIT (Just In Time), të quajtur avionëve , kryerjen e përpilimit sipas kërkesës. Ideja bazë është që kodi i makinës për një modul të caktuar krijohet “në fluturim”, në momentin kur thirret për ekzekutim për herë të parë. Sekuenca e përpunimit të programit mund të përfaqësohet nga diagrami i mëposhtëm.

Programi burimor

Përpilues

Përmbledhje

Përkthyes

Ekzekutimi

Kodi i makinës

Oriz. 2.6. Përmbledhje plus interpretim dhe jitting

Grupi i katërt i softuerit bazë formë programet e mirëmbajtjes së kompjuterit . Shumë programe të tilla, për shembull, përfshijnë programe diagnostikuese dhe zbuluese të gabimeve gjatë funksionimit të një kompjuteri ose rrjeti lokal.

2.1. Kontrollimi i arritjes së qëllimeve

2.2.Përgjigjet e sakta

3. Struktura e programit dhe rregullat e kodimit

Golat

Duke studiuar këtë element edukativ, ju do të jeni në gjendje të:

    interpretojnë saktë llojet dhe qëllimin e njësive të përkthimit;

    njohin strukturën e programit;

    të interpretojë saktë përkufizimin e një funksioni;

    të nxjerrë në pah komponentët strukturorë të përkufizimit të një funksioni;

    njohin deklaratat e objekteve të jashtme.

3.1. Informacione të përgjithshme

Një program është një algoritëm i shkruar në një gjuhë të kuptueshme për interpretuesin. Për një kompjuter, algoritmi duhet të shkruhet në një gjuhë programimi. Shkrimi i një algoritmi në një gjuhë programimi quhet kodim. Kur kodoni në një gjuhë programimi nivel të lartë Kodi burimor i programit shkruhet në fjali të veçanta. Zakonisht këto oferta janë:

    komente,

    përkufizimet dhe deklaratat,

    operatorët.

Komentet janë shpjegime për personin që do të shikojë tekstin e programit. Për të kryer veprimet e parashikuara nga ky algoritëm, ky lloj propozimi nuk nevojitet. Këto sugjerime mund të jenë të dobishme vetëm gjatë fazës së shikimit njerëzor të programit.

Përkthyesi fillon të formojë programin e makinës. Ai përkthen kodin burimor të një programi, të shkruar në një gjuhë programimi të nivelit të lartë, në një gjuhë të ndërmjetme ose gjuhë makine. Objekti i përpunuar në teksti burimor Programi mund të përfaqësohet nga një emër që formohet nga programuesi sipas gjykimit të tij. Një objekt i tillë është një bllok memorie që duhet të ruajë një vlerë të një lloji të përcaktuar. Prandaj, për të krijuar gjendjen fillestare të programit të punës, përkthyesi duhet të dijë të interpretojë emrin që has (d.m.th., cili objekt përfaqësohet me këtë emër).

Në mënyrë që përkthyesi të kryejë funksionet e tij, ai duhet të japë informacion për këtë objekt dhe të kërkojë krijimin e tij nëse objekti nuk është krijuar ende. Me krijimin e një objekti nënkuptojmë ndarjen e një blloku memorie për të dhe lidhjen e adresës së këtij blloku me emrin e objektit. Pasi të krijohet një objekt, emri i tij është një stenografi për adresën e bllokut të memories së objektit. Programuesi specifikon kërkesën për të krijuar një objekt në formën e një përkufizimi.

Përkufizimi objekti është një udhëzim për përkthyesin për të krijuar një objekt. Ai përmban informacione për emrin dhe llojin e tij. Lloji Një objekt karakterizohet nga një grup vlerash të vlefshme, një grup veprimesh të vlefshme mbi një vlerë, rregulla të shpërndarjes së memories dhe rregulla të ruajtjes së vlerës (rregullat e paraqitjes së memories). Prandaj, gjatë përcaktimit të një objekti sigurohuni që të tregoni në mënyrë eksplicite ose të nënkuptuar emrin dhe llojin e tij. Në këtë rast, lloji mund të përfaqësohet me një emër ose përshkrim. Zhvilluesit e përkthyesit përfshijnë në të emrat e disa llojeve të përdorura shpesh. Tani për tani, kushtojini vëmendje emrave të mëposhtëm të llojeve të paracaktuara:

Pra, lloji Numër i plotë përshkruan vlerat që përfaqësohen me numra të plotë (d.m.th. pa pjesë thyesore), Dyfishtë- një grup vlerash të përfaqësuara nga numrat me pjesë thyesore, Charvlerat simbolike, Boolean janë vlera logjike, dhe String janë vargje karakteresh, d.m.th. disa sekuencë personazhesh.

Para përpunimit të ndonjë objekti, ai duhet të krijohet, d.m.th. caktoni bllokun e duhur të memories dhe siguroni akses në të. Objekti mund të krijohet nga një përkthyes ose një programues. Në mënyrë që përkthyesi të krijojë një objekt, ai duhet të kalojë një udhëzim të caktuar që përmban informacion në lidhje me rregullat e shpërndarjes së kujtesës. Një udhëzim i tillë është shkruar në formën e një përkufizimi të objektit.

Në C++ dhe C#, përkufizimet shkruhen në fjali që mund të shfaqen në mënyrë sekuenciale në të njëjtën rresht ose në rreshta të veçantë. Në Visual Basic ato shkruhen vetëm në linja të veçanta test programi. Në cilëndo nga gjuhët e paraqitura, lejohet të specifikoni emrin e një objekti të vetëm ose një listë emrash në një përkufizim. Propozime të tilla formohen sipas shablloneve të mëposhtëm:

Gjuha C++:

<lloji> <Emri i objektit>;

<lloji> <Lista e emrave të objekteve>;

Gjuha C#:

<lloji> <Emri i objektit>;

<lloji> <Lista e emrave të objekteve>;

var<Emri i objektit>=<Inicializues>;

GjuhaVizualebazë:

Dim<Emri i objektit> Si<Shkruani emrin>

Dim<Lista e emrave të objekteve> Si<Shkruani emrin>

Dim<Emri i objektit>=<Inicializues>

Për shembull, dizajne

GjuhaVizualebazë: gjuha C++: Gjuha C#:

Dim m Si numër i plotë int m;

int m;

Dim n1, n2 Si numër i plotë int n1, n2; int n1, n2;

Dim x, w, z Si Double;

dyfishi x, w, z; dyfishi x, w, z;

Kuti dim=56 var kuti=56; janë përkufizimi i një objekti me numër të plotë (ndryshore) m, dy ndryshore të plota n1, n2 dhe tre ndryshore reale përkatësisht x, w, z. Shënim!++, Shënim!# Në gjuhë CVizualebazë përkufizimet shkruhen në çdo pozicion midis fjalive të tjera dhe përfundojnë me një pikëpresje . NË Çdo përkufizim nuk përmban një pikëpresje dhe shkruhet në një rresht të veçantë ose në një rresht të ndarë me dy pika. Në çdo rast, përkufizimetGjithmonë duhet

paraprijnë

pozicionet që tregojnë përdorimin e objekteve. Menjëherë pas krijimit, objekti nuk përmban një vlerë të dobishme për programuesin. Programuesi duhet ta vendosë këtë vlerë në mënyrë të pavarur. Për të kryer këtë veprim, përdoren operacionet e inicializimit, caktimit dhe hyrjes.

GjuhaVizualebazë: gjuha C++: Gjuha C#:

Operacioni i inicializimit – kjo është kopjimi i një vlere menjëherë pas ndarjes së memories. Tregohet drejtpërdrejt në përkufizimin e objektit duke përdorur simbolin = (baraz). Duket kështu: Dim a Si numër i plotë=60;

int a=60;

ose

int a(60); int a=60;

GjuhaVizualebazë: gjuha C++: Gjuha C#:

Dim b=700 var b=700;

Në këtë rast, ndryshorja A inicializohet në 60.Operatori i caktimit përfshin kopjimin e një vlere nga një burim i jashtëm. Çdo gjuhë mund të ketë grupin e vet të lehtësirave për kodimin e hyrjes së të dhënave.

Vizualebazë Hyrja e tastierës mund të kodohet duke përdorur blloqe dialogu ose metoda të klasës së konsolës. Për shembull, programi më poshtë ilustron aftësitë e futjes së të dhënave të kutisë së dialogut InputBox() dhe metodës Console.ReadLine().

Listimi 3.1.1.

"Ilustrimi i hyrjes dhe daljes me mjete të ndryshme

Zbeh emrin e përdoruesit si varg = InputBox ("Cili është emri yt?")

MsgBox ("Emri i përdoruesit: " & emri i përdoruesit)

Console.Write("Fut emrin e përdoruesit: ") : username = Console.ReadLine()

Console.WriteLine ("Emri i përdoruesit: (0)", emri i përdoruesit)

Console.ReadKey()

Mos harroni se metodat e klasës janë thjesht funksione që përcaktohen brenda klasës përkatëse. Për të hyrë në një metodë klase, specifikoni emrin e klasës, një pikë dhe emrin e metodës me kllapa, të cilat mund të jenë bosh ose të përmbajnë një listë argumentesh. Në mënyrë tipike, kur punoni me një dritare konsole, do të telefononi metodat Console.ReadLine() dhe Console.Read() për të koduar hyrjen e tastierës. E para nga këto metoda lexon një varg karakteresh, dhe e dyta - kodin e brendshëm të një karakteri të vetëm. Në metodë thirrjet brenda kllapa mos specifikoni asnjë argument, të dhënat e lexuara arrijnë në pikën e thirrjes së metodës. Kur përdorni mjete të ndryshme për futjen e të dhënave, duhet të mbani mend se thirrja e bllokut të hyrjes InputBox() çon në shfaqjen e një kutie dialogu me një redaktues me një rresht, në linjën e të cilit përdoruesi fut të dhënat që i nevojiten. Për shembull, për këtë program shfaqet një bllok dialogu, pamja e të cilit pas futjes së emrit "Volobuev" tregohet në Fig. 3.1.1.

Oriz. 3.1.1. Pamja e bllokut të dialogut të hyrjes pas futjes së një emri përdoruesi në linjën e redaktuesit

Meqenëse metoda Console.ReadLine() kthen një varg karakteresh që mund të përfaqësojnë tekstin ose imazhin e një numri, për të marrë vetë numrin, duhet të specifikoni në mënyrë eksplicite konvertimin e imazhit të tekstit në një numër. Për këtë qëllim, VB përdor metoda të klasës Convert ose makrot e veta si CInt(), CDbl(), etj. Një metodë e quajtur ToInt32(s) ose një makro CInt(s) thirret për të kthyer një varg karakteresh në një numër i plotë, dhe metoda ToDouble(s) ose makro CDbl(s) – në një numër real me saktësi të dyfishtë. Në këtë rast të veçantë, futja e një numri të plotë dhe një numri real dhe konvertimi në një lloj numerik duke përdorur metodat e klasës Convert duket si kjo:

Dim mm si numër i plotë= Convert.ToInt32(Console.ReadLine())

Dim m1 si dyfish = Convert.ToDouble(Console.ReadLine())

Kutia e zbehtë= Convert.ToDouble(Console.ReadLine())

Nëse përdorni makro, atëherë të njëjtët operatorë marrin formën

Zvogëlimi i mm si numër i plotë = CINT(Konsola.ReadLine())

Dim m1 si dyfish = CDbl(Console.ReadLine())

Kutia e zbehtë= CDbl(Konsola.ReadLine())

Shembull.

Shkruani një aplikacion konsol për të llogaritur sipërfaqen e një drejtkëndëshi të dhënë nga gjatësitë e brinjëve të tij. Gjatësitë e anëve futen nga tastiera.

Përgjigju(Lista 3.1.2).

Listimi 3.1.2.

"Llogaritja e sipërfaqes së një drejtkëndëshi

Console.Write("Fut gjatësinë e anës A të drejtkëndëshit: ")

Dim a Si Double = Convert.ToDouble(Console.ReadLine())

Console.Write ("Fut gjatësinë e anës B të drejtkëndëshit: ")

Dim b Si Double = Convert.ToDouble(Console.ReadLine())

Console.WriteLine ("Sipërfaqja e drejtkëndëshit: (0)", a * b)

Console.ReadKey()

Pas aktivizimit të aplikacionit, shfaqet dialogu i paraqitur në Fig. 3.1.2.

Oriz. 3.1.2. Forma e mundshme dialogu i shfaqur në dritaren e konsolës

Shënim!# Hyrja e tastierës kodohet nga metodat Console.ReadLine() dhe Console.Read(). Ato mund të thirren në të njëjtën formë siç përshkruhet për VB, por për konvertimin në një lloj numerik zakonisht mund të përdorni vetëm pajisjet e klasës Convert. Për shembull, nëse shkruani

int a, b, mm = Convert.ToInt32(Console.ReadLine());

double m1 = Convert.ToDouble(Console.ReadLine());

a=Convert.ToInt32(Console.ReadLine());

b=Convert.ToInt32(Console.ReadLine());

Operacioni i hyrjes mund të mos specifikohet domosdoshmërisht në kombinim me emrin e objektit. Për shembull, nëse aktivizoni aplikacionin, në formën e listimit 3.1.3

Listimi 3.1.3.

hapësira e emrit ConsAppSharp00 (

Programi i klasës (

Console.Title = "Input";!}

Console.WriteLine ("Enter a, b.");

// Titulli i dritares së konsolës

var a=Convert.ToInt32(Console.ReadLine());

Console.WriteLine("a=(0) b=(1)",a, Convert.ToInt32(Console.ReadLine()));

Console.ReadKey();

atëherë dritarja e konsolës do të ketë gjendjen e treguar në Fig. 3.1.3.

Shënim!++ Oriz. 3.1.3. Gjendja përfundimtare e dritares së konsolës/ ISO ANSI funksioni i hyrjes së tastierës kodohet nga një veprim i tërheqjes nga transmetimi cin

, i shkruar me simbolin '>>'. Për shembull, nëse shkruani

int mm; cin>>mm;

dyfishtë m1;

cin>>m1;

Shënim!++/ int a, b; cin>>a>>b; atëherë kjo do të thotë futja e vlerave të variablave mm, m1, a dhe b nga tastiera. funksioni i hyrjes së tastierës kodohet nga një veprim i tërheqjes nga transmetimi CLI

Operacioni i hyrjes së tastierës është i koduar nga metodat e klasës Console ashtu si në C# ose duke përdorur një transmetim

dhe operacionet ">>". Për shembull, nëse shkruani

int mm = Konverto::ToInt32(Konsola::ReadLine());

double m1 = Konverto::ToDouble(Konsola::ReadLine());

, i shkruar me simbolin '>>'. Për shembull, nëse shkruani

int mm; cin>>mm;

dyfishtë m1;

atëherë kjo do të thotë futja e vlerave të variablave mm, m1, a dhe b nga tastiera. Ashtu si C#, operacioni i hyrjes përmes thirrjeve të metodës së klasës së Konsolës mund të specifikohet opsionalisht në lidhje me emrin e objektit. Për shembull, nëse aktivizoni aplikacionin, në formën e listimit 3.1.4

Listimi 3.1.4.

// qq.cpp: skedari kryesor i projektit.

// Shembull i përdorimit të operacionit të hyrjes

#include "stdafx.h"

duke përdorur sistemin e hapësirës së emrave;

int main(arrit ^args) (

Konsola::Titulli = L"Hyrja e të dhënave";

Konsola::WriteLine(L"Enter a, b.");

// Titulli i dritares së konsolës

int a=Convert::ToInt32(Konsola::ReadLine());

Konsola::WriteLine(L"a=(0) b=(1)",a, Konverto::ToInt32(Konsola::ReadLine()));

Konsola::ReadKey(); Rezultati i ekzekutimit këtë aplikacion

do të ketë të njëjtën formë siç tregohet në Fig. 3.1.3.Tërheqja e operacionit A.<<’.

Vlerat e objekteve jo vetëm që mund të vendosen, por edhe të shfaqen në ekran. Çdo gjuhë programimi mund të ketë grupin e vet të mënyrave për të koduar këtë operacion. Megjithatë, për gjuhët që mbështesin platformën .NET, dalja në dritaren e konsolës mund të kodohet nga thirrjet në metodat e klasës Console. Këto shumë gjuhë përfshijnë VB, C# dhe C++/CLI. Në C++ ISO/ANSI, dalja në një dritare konsole mund të zbatohet duke futur në rrjedhën cout, e cila tregohet me simbolin 'mjedisi VB

Për dalje, mund të përdorni të dy blloqet e dialogut dhe mjetet e klasës Console. Një shumëllojshmëri e blloqeve të daljes së mesazhit përfshijnë, për shembull, një bllok të quajtur MsgBox(). Ky bllok ka disa modifikime. Në formën e tij më të thjeshtë, ai mund të përmbajë, brenda kllapave, një varg të vetëm karakteresh që përfaqësojnë mesazhin që do të shfaqet. Për shembull, për të shfaqur tekstin "Ky është programi im i parë". mjafton të tregoni thirrjen në këtë bllok në formular

MsgBox ("Ky është programi im i parë.")

Nëse një mesazh përbëhet nga disa pjesë, atëherë pjesët mund të bashkohen duke përdorur karakterin ampersand (&) në një varg të vetëm karakteresh. Për shembull, në programin e mësipërm, emri i përdoruesit i futur lidhet me vargun "Emri i përdoruesit:".

Pas thirrjes së një blloku të tillë dialogu, në ekran shfaqet një bllok dialogu, pamja e të cilit tregohet në Fig. 3.1.4.

Për të nxjerrë të dhëna në një dritare konsole duke përdorur mjetet .NET, thirrni metodat Console.WriteLine() dhe Console.Write(s[, list_elem]), të cilat shfaqin vargun e karaktereve të përfaqësuar nga parametri i parë s. Parametri i dytë është një listë e elementeve të përdorura për të modifikuar parametrin e parë pak para se të shfaqet. Kllapat katrore në këtë rast janë një simbol që tregon se këto parametra mund të hiqen. Në situatat më të thjeshta, kllapat mund të përdoren për të treguar tekstin e cituar ose një vlerë të vetme specifike ose emrin e një ndryshoreje ose shprehjeje të vetme

Console.WriteLine ("Vëllimi i kontejnerit")

Console.Write ("Kostoja e punës: ")

Console.WriteLine(235)

Console.Write(67+33)

Në mënyrë të ngjashme, çdo thirrje në metodat Console.ReadLine(), Console.Read() supozon se futja e të dhënave shoqërohet nga shfaqja e saj automatike në dritaren e konsolës. Për shembull, dialogu i futjes së të dhënave në programin e paraqitur në Listimin 3.1.1 shfaqet në dritaren e konsolës në formën e treguar në Fig. 3.1.5.

Oriz. 3.1.5. Gjendja e dritares së konsolës pas futjes së emrit të përdoruesit

Shënim!#DheShënim!++/ int a, b; cin>>a>>b; Operacioni i daljes në një dritare konsole specifikohet në të njëjtën mënyrë. Nëse operacioni shkruhet si një deklaratë e vetme, thirrja e metodës përfundon me një pikëpresje. Për shembull , në mjedisShënim!# operatorët

Console.WriteLine ("Vëllimi i kontejnerit");

Console.Write("Kosto e punës: ");

shfaqni tekstin midis thonjëzave në dritaren e konsolës. Dhe operatorët

Console.WriteLine(235);

Console.Write(67+33);

siguroni shfaqjen e numrave përkatësisht 235 dhe 100.

Operatorë të ngjashëm për të mërkurënShënim!++/ int a, b; cin>>a>>b; kanë formën:

Konsola::WriteLine(L"Vëllimi i kontejnerit");

Konsola::Write(L"Kosto e punës: ");

Konsola::WriteLine(235);

Konsola::Write(67+33);

Për shembull, ja se si mund të duket një aplikacion C# për të futur vlera nga tastiera dhe për të shfaqur vlerat në një dritare konsole.

Listimi 3.1.5.

// Ilustrimi i hyrjes/daljes së kodimit në një dritare konsole

// Programuesi A.F.

hapësira e emrit AppSh100 (

Programi i klasës (

zbrazëti statike kryesore (args vargu) (

Console.WriteLine("Fut një numër të plotë dhe një numër real."); // Këshillë për hyrje

int mm = Convert.ToInt32(Console.ReadLine());

// Fut një numër të plotë

double valueCap = Convert.ToDouble(Console.ReadLine()); // Fut një numër real

Console.WriteLine("Sum number=: (0)", mm + valueCap); // Nxjerr shumën e numrave

Console.ReadKey();

Oriz. 3.1.6. Gjendja përfundimtare e dritares së konsolës

Një aplikacion i thjeshtë C++/CLI mund të duket kështu:

Listimi 3.1.6.

// Programuesi A.F.

#include "stdafx.h" // Ruaje këtë lidhje në projektin tënd

duke përdorur sistemin e hapësirës së emrave;

int main(arrit ^args) (

// Deklarata e hapësirës së emrit

// Futni vlerat A dhe B në modalitetin e dialogut

Konsola::WriteLine(L"Enter A, B:");

double a = Convert::ToDouble(Console::ReadLine());

double b = Konverto::ToDouble(Konsola::ReadLine());

dyfishi y = Math::Sin(a+b);

// Përkufizimi i ndryshores y

// Nxjerr totalin në dritaren e konsolës

Konsola::WriteLine(L"Y= (0)", y);

Shënim!++ Oriz. 3.1.3. Gjendja përfundimtare e dritares së konsolës/ ISO Pas aktivizimit të një programi të tillë, dialogu me përdoruesin mund të jetë siç tregohet në Fig. 3.1.7 pamje:

Oriz. 3.1.7. Formulari i dialogut i krijuar nga aplikacioni<<"Объём контейнера"<

Oriz. 3.1.7. Formulari i dialogut i krijuar nga aplikacioni<<"Стоимость работ: ";

Oriz. 3.1.7. Formulari i dialogut i krijuar nga aplikacioni<<(67+33);

Dalja në dritaren e konsolës kodohet nga një operacion futjeje në rrymën cout. Për shembull, deklaratat e paraqitura më sipër mund të shkruhen në formë

Listimi 3.1.7.

cout

Një shembull i kodimit të një aplikacioni të thjeshtë është programi i mëposhtëm:

/* Llogaritni vlerën e një shprehjeje */ #include "stdafx.h" // Mbajeni gjithmonë këtë lidhje në projektin tuaj

/* Llogaritni vlerën e një shprehjeje */ #përfshi

// Për të hyrë në funksionin Sin(x).

// Lidhja e pajisjeve I/O

duke përdorur hapësirën e emrave std;

// Deklarimi i hapësirës së emrave std

Oriz. 3.1.7. Formulari i dialogut i krijuar nga aplikacioni<<"Введите A, B: "; cin >int _tmain(int argc, _TCHAR* argv)(

dyfishi a, b; // Përkufizimi i ndryshoreve reale a, b

setlocale (LC_CTYPE, "rusisht"); // Vendosni daljen në cirilik

>a >> b;

// Hyrja e tastierës<< "\nY= " <

cin.get(); // Për të lexuar Enterin përfundimtar, shtypni

/* Llogaritni vlerën dhe shfaqeni në ekran */

dyfishi y = sin(a+b); // Përkufizimi i një ndryshoreje reale y

cout cin.get(); // Ndalo për të parë totalin Gjatë gjenerimit të tekstit të këtij programi, komentet me shumë rreshta tregojnë qëllimin e programit dhe theksojnë fazën e llogaritjes dhe shfaqjes së vlerës në ekran. Të gjitha komentet e tjera janë me një linjë. Pas aktivizimit të një programi të tillë, dialogu me përdoruesin mund të duket si ai i paraqitur në figurën 3.1.8. Oriz. 3.1.8. Pamja përfundimtare e dritares së konsolës së aplikacionit . Nga kjo rrjedh se një program në këto gjuhë mund të përfaqësohet nga një ose më shumë skedarë të kodit burimor, secila prej të cilave duhet t'i jepet hyrjes së përkthyesit. Çdo skedar që përmban kodin burimor të të gjithë ose një pjese të një programi mund të përfshijë një sekuencë përkufizimesh të ndërfaqeve, klasave, delegatëve dhe strukturave. Për VB, skedari mund të përmbajë module. Brenda çdo moduli, ju mund të deklaroni dhe përcaktoni lloje, konstante, variabla dhe nënprograme. Për C++, një skedar mund të përmbajë gjithashtu një grup funksionesh, kufijtë e të cilëve nuk mbivendosen.

Përkujtim!

Strukturat – llojet, të cilat janë versione të thjeshtuara të klasave.

Delegatët - klasa speciale të krijuara për të përshkruar objekte që mund të bëhen një mjet për thirrjen e metodave të lidhura me to.

Ndërfaqet- një lloj i veçantë i klasave abstrakte që përshkruajnë zyrtarisht titujt e aksesorëve të burimeve, dhe lloji i numëruar ka për qëllim të përshkruajë objektet, vlerat e të cilave janë renditur nga programuesi kur përshkruan llojin.

3.2. Struktura e programit dhe nënprogramet

Një program Visual Basic është ndërtuar nga blloqe ndërtimi - zgjidhje, projekte, asamble, skedarë burimi, module, klasa dhe ndërfaqe. Një zgjidhje përbëhet nga një ose më shumë projekte. Projekti, nga ana tjetër, mund të përmbajë një ose më shumë asamble. Çdo asamble është përpiluar nga një ose më shumë skedarë burimi. Skedari burim përfshin përkufizimet dhe zbatimin e klasave, strukturave, moduleve dhe ndërfaqeve, dhe në fund përmban të gjithë kodin.

PUNË LABORATORIKE 3. STUDIMI I FAZËS SË ANALIZËS LEKSIKE TË PROGRAMIMIT TË PËRKTHYESVE TË GJUHËS

Qëllimi i punës: Metodat e studimit për ndërtimin e skanerëve leksikor bazuar në makinat e gjendjeve të fundme dhe gramatikat formale.

Arkitektura e përpiluesit

Programi burimor, i shkruar në ndonjë gjuhë programimi, nuk është gjë tjetër veçse një zinxhir karakteresh. Një program që përkthen një program nga një gjuhë e nivelit të lartë në një gjuhë objekti ekuivalente quhet përkthyes. Nëse ndodh përkthimi në kodet e makinës, atëherë përkthyesi quhet përpilues. Kompiluesi e kthen këtë varg karakteresh në një varg bitësh - kodi i objektit. Është shumë i përshtatshëm për të ndarë procesin e përpilimit logjikisht në disa faza të njëpasnjëshme:

Përpunim paraprak;

Analiza leksikore;

Parsing;

gjenerimi i kodeve;

Optimizimi i programit.

Le të hedhim një vështrim më të afërt në fazën e analizës leksikore. Programi që kryen këtë hap quhet analizues leksikor ose skaner. Skaneri përkthen tekstin e programit burim nga një sekuencë karakteresh ose rreshtash në një sekuencë shenjash. Një leksemë është një element minimal i një gjuhe programimi. Për një gjuhë programimi të caktuar, numri i llojeve të shenjave supozohet të jetë i kufizuar.

Pasi të njihen shenjat, informacioni për disa prej tyre mblidhet dhe regjistrohet në një ose më shumë tabela. Analiza leksikore mund të kryhet me një vështrim në tekstin burimor. Pas tij, programi merr një formë të ndërmjetme. Gjatë analizës leksikore zbulohen dhe shënohen gabimet leksikore. Komentet gjithashtu hidhen poshtë gjatë kësaj faze.

Analiza leksikore

Analiza leksikore njeh tre lloje të njësive leksikore: simbolet fundore, identifikuesit e mundshëm dhe fjalëpërfjalë. Së pari, të gjitha njësitë leksikore krahasohen me elementet e tabelës së simboleve terminale. Nëse ka një përputhje, ato vendosen në tabelën standarde të simboleve. Çdo simbol standard përmban një tregues drejt një tabele, elementi i të cilit është njësia leksikore përkatëse dhe indeksi i tij në këtë tabelë.

Pasi një njësi leksikore klasifikohet si "identifikues i mundshëm", pyetet tabela e identifikuesve nëse nuk ka një zë të tillë në tabelë, krijohet një element i ri. Pjesa tjetër e informacionit në tabelë futet në fazat vijuese.

Numrat, vargjet e karaktereve të mbyllura në thonjëza dhe të dhëna të tjera të vetëpërcaktuara klasifikohen si "literale". Informacioni rreth tyre futet në tabelën e mirëfilltë. Ndryshe nga identifikuesit, literalet ju lejojnë të përcaktoni atributet e tyre.

Le të shqyrtojmë një shembull të ndërtimit të tabelave të përshkruara gjatë përpilimit të programit të mëposhtëm:

char * b=".dat";

Tabela e karaktereve të terminalit (TRM). Tabela e simboleve standarde.

Simboli Ndarës Të tjera Lloji Indeksi Linja e programit
; TRM kryesore
( TRM (
) TRM )
, TRM {
kryesore TRM ndër
ndër IDN a
{ TRM ;
} TRM karakter
= TRM *
+ IDN b
* TRM =
karakter TRM "
" LTR .dat
TRM "
IDN a
TRM =
IDN a
TRM +
LTR
TRM ;
TRM }

Tabela identifikuese (IDN). Tabela literale (LTR).

Tabelat e diskutuara më sipër demonstrojnë rregullat bazë të ndërtimit. Tabela e simboleve të terminalit tashmë është përfshirë në kompajler dhe në këtë rast përmbajtja e saj është ndërtuar posaçërisht për shembull.

Mënyra më e thjeshtë për të organizuar një tabelë simbolesh është të shtoni elementë sipas radhës që arrijnë, por më pas duhet të shpenzoni shumë kohë duke kërkuar. Elementet e tabelës mund të renditen dhe të përdoren metoda të ndryshme kërkimi.



Gjuhët e programimit të nivelit të lartë kanë një strukturë blloqesh dhe procedurash të mbivendosur. I njëjti identifikues mund të përshkruhet dhe përdoret në blloqe të ndryshme. Rregulli për gjetjen e një përshkrimi që përputhet me një identifikues është që së pari të shikoni bllokun aktual (në të cilin përdoret identifikuesi), pastaj bllokun rrethues, e kështu me radhë. derisa të gjendet një përshkrim i këtij identifikuesi. Për këtë përdoret një listë e blloqeve.

Tabela e bllokut.

Skaneri leksikor duhet të marrë parasysh shtrirjet dhe t'i kodojë ato ndryshe.

Përmbajtja e detyrës: Zhvilloni një program skanimi dhe analize leksikore për një gjuhë programimi të caktuar dhe llojet e shenjave. Programi duhet të ndërtojë tabelat e dhëna dhe, mbi bazën e tyre, të transformojë programin e analizuar, duke zëvendësuar leksema të kërkuara me emra mnemonikë. Emrat mnemonikë duhet të krijohen në mënyrë që çdo shenjë të zëvendësohet me një emër unik dhe emri të pasqyrojë llojin e tij (për shembull, I1 është shenja e parë e një lloji të plotë).



Ju pëlqeu artikulli? Ndani me miqtë tuaj!