Programiranje

Leksikalna analiza, 2. del: Izdelava aplikacije

Prejšnji mesec sem si ogledal razrede, ki jih ponuja Java za osnovno leksikalno analizo. Ta mesec se bom sprehodil po preprosti aplikaciji, ki uporablja StreamTokenizer za uporabo interaktivnega kalkulatorja.

Za kratek pregled prejšnjemesečnega članka sta na voljo dva razreda leksikalnih analizatorjev, ki sta vključena v standardno distribucijo Jave: StringTokenizer in StreamTokenizer. Ti analizatorji pretvorijo svoj vložek v diskretne žetone, ki jih razčlenjevalnik lahko uporabi za razumevanje določenega vhoda. Razčlenjevalnik izvaja slovnico, ki je opredeljena kot eno ali več ciljnih stanj, doseženih z ogledom različnih zaporedij žetonov. Ko je doseženo ciljno stanje razčlenjevalca, izvede nekaj dejanj. Ko razčlenjevalnik zazna, da glede na trenutno zaporedje žetonov ni možnih stanj ciljev, to opredeli kot stanje napake. Ko razčlenjevalnik doseže stanje napake, izvede obnovitveno dejanje, s katerim razčlenjevalnik vrne na točko, na kateri lahko začne znova razčleniti. Običajno se to izvede z zajemanjem žetonov, dokler se razčlenjevalnik ne vrne na veljavno izhodišče.

Prejšnji mesec sem vam pokazal nekaj metod, ki so uporabljale a StringTokenizer za razčlenitev nekaterih vhodnih parametrov. Ta mesec vam bom pokazal aplikacijo, ki uporablja StreamTokenizer objekt za razčlenitev vhodnega toka in izvajanje interaktivnega kalkulatorja.

Izdelava aplikacije

Naš primer je interaktivni kalkulator, ki je podoben ukazu Unix bc (1). Kot boste videli, potiska StreamTokenizer razred do roba njegove uporabnosti kot leksikalni analizator. Tako služi kot dober prikaz, kje je mogoče potegniti mejo med "preprostimi" in "zapletenimi" analizatorji. Ta primer je aplikacija Java in zato najbolje teče iz ukazne vrstice.

Kalkulator kot hiter povzetek svojih sposobnosti sprejme izraze v obliki

[ime spremenljivke] "=" izraz 

Ime spremenljivke ni obvezno in je lahko kateri koli niz znakov v privzetem obsegu besed. (Za program osvežite spomin na te znake lahko uporabite programček vadbe iz prejšnjega meseca.) Če je ime spremenljivke izpuščeno, se vrednost izraza preprosto natisne. Če je prisotno ime spremenljivke, je spremenljivki dodeljena vrednost izraza. Ko so spremenljivke dodeljene, jih je mogoče uporabiti v poznejših izrazih. Tako zapolnijo vlogo "spominov" na sodobnem ročnem kalkulatorju.

Izraz je sestavljen iz operandov v obliki številskih konstant (dvojne natančnosti, konstante s plavajočo vejico) ali imen spremenljivk, operatorjev in oklepajev za razvrščanje določenih izračunov. Pravni operaterji so seštevanje (+), odštevanje (-), množenje (*), deljenje (/), bitni AND (&), bitni ALI (|), bitni XOR (#), stopnjevanje (^) in unarno negacijo bodisi z minusom (-) za rezultat dopolnitve dvojic ali z udarcem (!) za rezultat dopolnitve dveh.

Poleg teh izjav lahko naša aplikacija kalkulatorja sprejme tudi enega od štirih ukazov: "dump", "clear", "help" in "quit". The smetišče ukaz izpiše vse trenutno spremenljivke in njihove vrednosti. The jasno ukaz izbriše vse trenutno definirane spremenljivke. The pomoč ukaz natisne nekaj vrstic besedila pomoči, da uporabnik zažene. The prenehati ukaz povzroči, da se aplikacija zapre.

Celotna aplikacija je sestavljena iz dveh razčlenjevalnikov - enega za ukaze in stavke in enega za izraze.

Izdelava razčlenjevalnika ukazov

Razčlenjevalnik ukazov je izveden v aplikacijskem razredu za primer STExample.java. (Kazalec na kodo najdete v razdelku Viri.) glavni metoda za ta razred je opredeljena spodaj. Sprehodil se bom po kosih zate.

 1 javna statična void main (String args []) vrže IOException {2 Hashtable spremenljivke = new Hashtable (); 3 StreamTokenizer st = nov StreamTokenizer (System.in); 4 st.eolIsSignificant (true); 5 st.lowerCaseMode (true); 6. st.ordinaryChar ('/'); 7. st.ordinaryChar ('-'); 

V zgornji kodi najprej dodam a java.util.Hashtable razred za shranjevanje spremenljivk. Po tem dodelim a StreamTokenizer in ga nekoliko prilagodite glede na privzete vrednosti. Utemeljitev sprememb je naslednja:

  • eolIspomemben je nastavljeno na prav tako da bo tokenizer vrnil oznako konca vrstice. Konec vrstice uporabljam kot točko, kjer se izraz konča.

  • lowerCaseMode je nastavljeno na prav tako da bodo imena spremenljivk vedno vrnjena z malimi črkami. Na ta način imena spremenljivk ne razlikujejo med velikimi in malimi črkami.

  • Poševnica (/) je nastavljena na navaden znak, tako da ne bo uporabljena za označevanje začetka komentarja in jo lahko namesto tega uporabite kot operater delitve.

  • Znak minus (-) je nastavljen na običajni znak, tako da se niz "3-3" segmentira v tri žetone - "3", "-" in "3" - in ne samo na "3" in "-3." (Ne pozabite, da je razčlenjevanje številk privzeto nastavljeno na »on«.)

Ko je tokenizer nastavljen, razčlenjevalnik ukazov teče v neskončni zanki (dokler ne prepozna ukaza "prenehaj", na kateri točki zapusti). To je prikazano spodaj.

 8 while (true) {9 Izraz res; 10 int c = StreamTokenizer.TT_EOL; 11 niz varName = null; 12 13 System.out.println ("Vnesite izraz ..."); 14 poskusite {15 while (true) {16 c = st.nextToken (); 17 if (c == StreamTokenizer.TT_EOF) {18 System.exit (1); 19} sicer če (c == StreamTokenizer.TT_EOL) {20 nadaljevanje; 21} else if (c == StreamTokenizer.TT_WORD) {22 if (st.sval.compareTo ("dump") == 0) {23 dumpVariables (spremenljivke); 24 nadaljevati; 25} else if (st.sval.compareTo ("clear") == 0) {26 variables = new Hashtable (); 27 nadaljevati; 28} else if (st.sval.compareTo ("quit") == 0) {29 System.exit (0); 30} else if (st.sval.compareTo ("exit") == 0) {31 System.exit (0); 32} else if (st.sval.compareTo ("help") == 0) {33 help (); 34 nadaljevati; 35} 36 varName = st.sval; 37 c = st.nextToken (); 38} 39 odmor; 40} 41 if (c! = '=') {42 vrzi novo SyntaxError ("manjka začetnica '=' znak."); 43} 

Kot lahko vidite v vrstici 16, se prvi žeton pokliče s klicem nextToken na StreamTokenizer predmet. To vrne vrednost, ki označuje vrsto žetona, ki je bil optično prebran. Vrnjena vrednost bo ena od definiranih konstant v StreamTokenizer razred ali pa bo to vrednost znaka. Žetoni "meta" (tisti, ki niso samo vrednosti znakov) so opredeljeni na naslednji način:

  • TT_EOF - To pomeni, da ste na koncu vhodnega toka. Za razliko StringTokenizer, ni hasMoreTokens metoda.

  • TT_EOL - To vam pove, da je objekt pravkar prenesel zaporedje konca vrstice.

  • TT_NUMBER - Ta vrsta žetona sporoča kodi razčlenjevalnika, da je bila na vhodu vidna številka.

  • TT_WORD - Ta vrsta žetona pomeni, da je bila skenirana celotna "beseda".

Če rezultat ni ena od zgornjih konstant, je bodisi vrednost znaka, ki predstavlja znak v "običajnem" obsegu znakov, ki je bila optično prebrana, ali eden od narekovajev, ki ste jih nastavili. (V mojem primeru ni nastavljen noben znak narekovaja.) Ko je rezultat eden od vaših narekovajev, lahko naveden niz najdemo v spremenljivki primerka niza. sval od StreamTokenizer predmet.

Koda v vrsticah od 17 do 20 obravnava navedbe konca vrstice in konca datoteke, medtem ko je v vrstici 21 postavka if uporabljena, če je bil vrnjen besedni žeton. V tem preprostem primeru je beseda bodisi ukaz bodisi ime spremenljivke. Vrstice od 22 do 35 obravnavajo štiri možne ukaze. Če je dosežena vrstica 36, ​​mora biti ime spremenljivke; posledično program obdrži kopijo imena spremenljivke in dobi naslednji žeton, ki mora biti znak enačbe.

Če v vrstici 41 žeton ni enakovreden znak, naš preprost razčlenjevalnik zazna stanje napake in vrže izjemo, da to sporoči. Ustvaril sem dve splošni izjemi, SyntaxError in ExecError, da ločimo napake pri razčlenjevanju in napake med izvajanjem. The glavni metoda se nadaljuje s spodnjo vrstico 44.

44 res = ParseExpression.expression (st); 45} catch (SyntaxError se) {46 res = null; 47 varName = null; 48 System.out.println ("\ nZaznana napaka sintakse! -" + se.getMsg ()); 49 while (c! = StreamTokenizer.TT_EOL) 50 c = st.nextToken (); 51 nadaljevati; 52} 

V vrstici 44 je izraz na desni strani enačbe razčlenjen z razčlenjevalnikom izrazov, definiranim v ParseExpression razred. Upoštevajte, da so vrstice od 14 do 44 ovite v blok try / catch, ki zajame sintaksne napake in jih obravnava. Ko je zaznana napaka, mora razčlenjevalnik obnoviti vse žetone do vključno žetona konca vrstice. To je prikazano v vrsticah 49 in 50 zgoraj.

Na tej točki, če izjema ni bila vržena, je aplikacija uspešno razčlenila stavek. Končno preverjanje je, da je naslednji žeton konec vrstice. Če ni, napaka ni zaznana. Najpogostejša napaka bodo neusklajene oklepaje. To preverjanje je prikazano v vrsticah od 53 do 60 spodnje kode.

53 c = st.nextToken (); 54 if (c! = StreamTokenizer.TT_EOL) {55 if (c == ')') 56 System.out.println ("\ nZaznana napaka sintakse! - Za številne zapiralne pare."); 57 else 58 System.out.println ("\ nBogusov žeton na vhodu -" + c); 59 while (c! = StreamTokenizer.TT_EOL) 60 c = st.nextToken (); 61} drugo { 

Ko je naslednji žeton konec vrstice, program izvede vrstice od 62 do 69 (prikazano spodaj). Ta odsek metode oceni razčlenjen izraz. Če je bilo ime spremenljivke nastavljeno v vrstici 36, se rezultat shrani v tabelo simbolov. V obeh primerih, če se ne vrne nobena izjema, se izraz in njegova vrednost natisneta v tok System.out, tako da lahko vidite, kaj je razčlenjeval razčlenjevalnik.

62 poskusite {63 Double z; 64 System.out.println ("Razčlenjen izraz:" + res.unparse ()); 65 z = novo Double (res.value (spremenljivke)); 66 System.out.println ("Vrednost je:" + z); 67 if (varName! = Null) {68 variables.put (varName, z); 69 System.out.println ("Dodeljeno:" + varName); 70} 71} catch (ExecError ee) {72 System.out.println ("Napaka pri izvedbi," + ee.getMsg () + "!"); 73} 74} 75} 76} 

V Npr razred, StreamTokenizer uporablja razčlenjevalnik ukaznih procesorjev. Ta vrsta razčlenjevalnika se običajno uporablja v lupinskem programu ali v kateri koli situaciji, v kateri uporabnik interaktivno oddaja ukaze. Drugi razčlenjevalnik je vdelan v ParseExpression razred. (Za popoln vir glejte razdelek Viri.) Ta razred razčleni izračune kalkulatorja in se prikliče v zgornji vrstici 44. Tukaj je to StreamTokenizer se sooča z najtežjim izzivom.

Izdelava razčlenjevalnika izrazov

Slovnica izrazov kalkulatorja opredeljuje algebraično sintakso oblike "[element] operator [item]." Ta vrsta slovnice se vedno znova pojavlja in se imenuje operater slovnica. Priročen zapis za slovnico operatorja je:

id ("OPERATOR" id) * 

Zgornja koda bi se glasila "ID terminal, ki mu sledi nič ali več pojavitev kompleta ID-ja operaterja." The StreamTokenizer class bi se zdela precej idealna za analizo takšnih tokov, ker zasnova naravno razbije vhodni tok beseda, številko, in navaden lik žetoni. Kot vam bom pokazal, to do neke mere drži.

The ParseExpression class je neposreden razčlenjevalnik izrazov z rekurzivnim spustom, takoj iz razreda dodiplomskega prevajalskega oblikovanja. The Izraz metoda v tem razredu je opredeljena na naslednji način:

 1 statični izraz izraza (StreamTokenizer st) vrže SyntaxError {2 Rezultat izraza; 3 logične vrednosti = false; 4 5 rezultat = vsota (st); 6 while (! Done) {7 poskusite {8 switch (st.nextToken ()) 9 case '&': 10 result = new Expression (OP_AND, result, sum (st)); 11 odmor; 12 case '23} catch (IOException ioe) {24 throw new SyntaxError ("Dobil I / O izjemo."); 25} 26} 27 vrnjeni rezultat; 28} 
$config[zx-auto] not found$config[zx-overlay] not found