Ohjelmointi

Leksikaalinen analyysi, osa 2: Rakenna sovellus

Viime kuussa katsoin luokkia, jotka Java tarjoaa perustavanlaatuisen leksikaalisen analyysin tekemiseksi. Tässä kuussa käyn läpi yksinkertaisen sovelluksen, joka käyttää StreamTokenizer toteuttaa interaktiivinen laskin.

Viime kuussa julkaistun artikkelin tarkastelemiseksi lyhyesti on kaksi leksikaalianalysaattoriluokkaa, jotka sisältyvät tavalliseen Java-jakeluun: StringTokenizer ja StreamTokenizer. Nämä analysaattorit muuttavat syötteensä erillisiksi tunnuksiksi, joita jäsentäjä voi käyttää tietyn syötteen ymmärtämiseen. Jäsennys toteuttaa kieliopin, joka määritellään yhdeksi tai useammaksi tavoitetilaksi, jotka saavutetaan näkemällä erilaisia ​​tunnussarjoja. Kun jäsentäjän tavoitetila saavutetaan, se suorittaa jonkin toiminnon. Kun jäsennin havaitsee, että nykyisen tunnussekvenssin perusteella ei ole mahdollista tavoitetilaa, se määrittelee tämän virhetilaksi. Kun jäsennin saavuttaa virhetilan, se suorittaa palautustoiminnon, joka vie jäsentimen takaisin pisteeseen, jossa se voi aloittaa jäsentämisen uudelleen. Tyypillisesti tämä toteutetaan kuluttamalla tunnuksia, kunnes jäsennin on palannut kelvolliseen aloituspisteeseen.

Viime kuussa näytin sinulle joitain menetelmiä, joissa käytettiin a StringTokenizer jäsentää joitain syöttöparametreja. Tässä kuussa näytän sovelluksen, joka käyttää a StreamTokenizer objekti jäsentää tulovirta ja toteuttaa interaktiivinen laskin.

Sovelluksen rakentaminen

Esimerkkimme on interaktiivinen laskin, joka on samanlainen kuin Unix bc (1) -komento. Kuten näette, se työntää StreamTokenizer luokka aivan sen käyttökelpoisuuden reunaan leksikaalisena analysaattorina. Siten se toimii hyvänä osoituksena siitä, mihin linja "yksinkertaisten" ja "monimutkaisten" analysaattorien välille voidaan vetää. Tämä esimerkki on Java-sovellus ja toimii siksi parhaiten komentoriviltä.

Nopeana yhteenvetona kyvyistään laskin hyväksyy lausekkeet muodossa

[muuttujan nimi] "=" lauseke 

Muuttujan nimi on valinnainen, ja se voi olla mikä tahansa merkkijono oletussanan alueelta. (Voit päivittää muistisi näillä merkeillä viime kuun artikkelin kuntosovelluksen avulla.) Jos muuttujan nimi jätetään pois, lausekkeen arvo yksinkertaisesti tulostetaan. Jos muuttujan nimi on läsnä, lausekkeen arvo määritetään muuttujalle. Kun muuttujat on osoitettu, niitä voidaan käyttää myöhemmissä lausekkeissa. Siten ne täyttävät "muistojen" roolin modernilla käsilaskimella.

Lauseke koostuu operanteista numeeristen vakioiden (kaksoistarkkuus-, liukulukuvakiot) tai muuttujien nimien, operaattorien ja sulkeiden muodossa tiettyjen laskelmien ryhmittelyä varten. Laillisia operaattoreita ovat yhteenlasku (+), vähennyslasku (-), kertolasku (*), jako (/), bitti JA (&), bitti OR (|), bitti XOR (#), eksponentti (^) ja unaarinen negaatio joko miinus (-) kahdelle täydennystulokselle tai bang (!) täydennystulokselle.

Näiden lausekkeiden lisäksi laskinsovelluksemme voi myös ottaa yhden neljästä komennosta: "dump", "clear", "help" ja "quit". kaataa komento tulostaa kaikki määritetyt muuttujat sekä niiden arvot. asia selvä komento poistaa kaikki tällä hetkellä määritetyt muuttujat. auta komento tulostaa muutaman rivin ohjetekstiä käyttäjän aloittamiseksi. lopettaa -komento saa sovelluksen poistumaan.

Koko esimerkkisovellus koostuu kahdesta jäsentimestä - yksi komennoille ja lauseille ja toinen lausekkeille.

Rakennetaan komento jäsennin

Komennon jäsennin toteutetaan sovellusluokassa esimerkille STExample.java. (Katso Resurssit-osiosta osoitin koodille.) tärkein kyseisen luokan menetelmä on määritelty alla. Käyn läpi palaset sinulle.

 1 julkinen staattinen void main (String args []) heittää IOException {2 Hashtable-muuttujat = new Hashtable (); 3 StreamTokenizer st = uusi StreamTokenizer (System.in); 4 st.eolIsSignificant (tosi); 5 st. AlempiCaseMode (true); 6. ylimääräinen Char ('/'); 7. ylimääräinen Char ('-'); 

Yllä olevassa koodissa ensimmäinen asia, jonka teen, on a java.util.Hashtable luokka pitämään muuttujat. Sen jälkeen varataan a StreamTokenizer ja säädä sitä hieman oletusasetuksistaan. Muutosten perustelut ovat seuraavat:

  • eolIsTärkeää on asetettu totta niin, että tokenizer palauttaa merkinnän rivin lopusta. Käytän rivin päätä lausekkeen loppupisteenä.

  • lowerCaseMode on asetettu totta niin, että muuttujien nimet palautetaan aina pienillä kirjaimilla. Tällä tavalla muuttujien nimet eivät eroa isoja ja pieniä kirjaimia.

  • Viistomerkki (/) on asetettu tavalliseksi merkiksi, jotta sitä ei käytetä osoittamaan kommentin alkua, ja sitä voidaan käyttää jako-operaattorina.

  • Miinusmerkki (-) on asetettu tavalliseksi merkiksi, jotta merkkijono "3-3" segmentoidaan kolmeen tunnukseen - "3", "-" ja "3" - eikä vain "3" ja "-3." (Muista, että numeron jäsentäminen on oletusarvoisesti "päällä".)

Kun tokenizer on asetettu, komento jäsennin toimii loputtomassa silmukassa (kunnes se tunnistaa "quit" -komennon, josta se poistuu). Tämä näkyy alla.

 8 kun (tosi) {9 Lauseke; 10 int c = StreamTokenizer.TT_EOL; 11 Merkkijono varName = null; 12 13 System.out.println ("Kirjoita lauseke ..."); 14 kokeile {15 kun (tosi) {16 c = st.nextToken (); 17 if (c == StreamTokenizer.TT_EOF) {18 System.exit (1); 19} muu jos (c == StreamTokenizer.TT_EOL) {20 jatkuu; 21} else if (c == StreamTokenizer.TT_WORD) {22 if (st.sval.compareTo ("dump") == 0) {23 dumpVariables (muuttujat); 24 jatka; 25} else if (st.sval.compareTo ("clear") == 0) {26 muuttujaa = uusi Hashtable (); 27 jatkuu; 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 jatka; 35} 36 varName = st.sval; 37 c = st.sextToken (); 38} 39 tauko; 40} 41 if (c! = '=') {42 heittää uusi SyntaxError ("puuttuu alkukirjain = = merkki".); 43} 

Kuten näet riviltä 16, ensimmäinen tunnus kutsutaan kutsumalla seuraavaToken on StreamTokenizer esine. Tämä palauttaa arvon, joka osoittaa skannatun tunnuksen tyypin. Palautusarvo on joko yksi määritellyistä vakioista StreamTokenizer luokka tai se on merkin arvo. "Meta" -merkit (ne, jotka eivät ole pelkästään merkin arvoja) määritellään seuraavasti:

  • TT_EOF - Tämä tarkoittaa, että olet tulovirran lopussa. Toisin kuin StringTokenizer, ei ole hasMoreTokens menetelmä.

  • TT_EOL - Tämä kertoo sinulle, että objekti on juuri ohittanut rivin lopun.

  • TT_NUMBER - Tämä tunnustyyppi kertoo jäsenninkoodillesi, että luku on nähty syötteessä.

  • TT_WORD - Tämä tunnustyyppi tarkoittaa, että koko "sana" on skannattu.

Kun tulos ei ole yksi yllä olevista vakioista, se on joko skannattu "tavallisen" merkkialueen merkkiä edustava merkkiarvo tai jokin asettamistasi lainausmerkeistä. (Minun tapauksessani lainausmerkkiä ei ole asetettu.) Kun tulos on yksi lainausmerkeistäsi, lainattu merkkijono löytyy merkkijonon muuttujasta sval n StreamTokenizer esine.

Rivien 17 - 20 koodi käsittelee rivin lopussa ja tiedostossa olevat merkinnät, kun taas rivillä 21 otetaan if-lauseke, jos sanatunnus palautettiin. Tässä yksinkertaisessa esimerkissä sana on joko komennon tai muuttujan nimi. Rivit 22-35 käsittelevät neljää mahdollista komentoa. Jos rivi 36 saavutetaan, sen on oltava muuttujan nimi; näin ollen ohjelma säilyttää kopion muuttujan nimestä ja saa seuraavan tunnuksen, jonka on oltava tasa-arvoinen merkki.

Jos rivillä 41 merkki ei ollut tasa-arvoinen merkki, yksinkertainen jäsentimemme havaitsee virhetilan ja heittää poikkeuksen ilmoittamaan siitä. Loin kaksi yleistä poikkeusta, SyntaksiVirhe ja ExecError, erottaa jäsentelyaikavirheet ajonaikaisista virheistä. tärkein menetelmä jatkuu rivillä 44 alla.

44 res = ParseExpression.lauseke (st); 45} saalis (SyntaxError se) {46 res = null; 47 varName = tyhjä; 48 System.out.println ("\ nSintaksivirhe havaittu! -" + se.getMsg ()); 49 kun (c! = StreamTokenizer.TT_EOL) 50 c = st.nextToken (); 51 jatka; 52} 

Rivillä 44 yhtälön oikealla puolella oleva lauseke jäsennetään lausekkeen jäsentimellä, joka on määritelty ParseExpression luokassa. Huomaa, että rivit 14 - 44 on kääritty try / catch-lohkoon, joka vangitsee syntaksivirheet ja käsittelee niitä. Kun havaitaan virhe, jäsentäjän palautustoiminto on kuluttaa kaikki tunnukset seuraavaan rivin loppuun asti. Tämä näkyy riveillä 49 ja 50 yllä.

Jos tässä vaiheessa ei tehty poikkeusta, sovellus on jäsennellyt lauseen. Viimeinen tarkistus on nähdä, että seuraava tunnus on rivin loppu. Jos se ei ole, virhettä ei ole havaittu. Yleisin virhe on väärät sulut. Tämä tarkistus näkyy alla olevan koodin riveillä 53--60.

53 c = st.sextToken (); 54 if (c! = StreamTokenizer.TT_EOL) {55 if (c == ')') 56 System.out.println ("\ nSyntaksivirhe havaittu! - Monille sulkeville pareneille."); 57 muu 58 System.out.println ("\ nVarmennemerkki syötteessä -" + c); 59 taas (c! = StreamTokenizer.TT_EOL) 60 c = st.nextToken (); 61} muu { 

Kun seuraava tunniste on rivin loppu, ohjelma suorittaa rivit 62 - 69 (esitetty alla). Tämä menetelmän osa arvioi jäsennetyn lausekkeen. Jos muuttujan nimi asetettiin riville 36, tulos tallennetaan symbolitaulukkoon. Kummassakin tapauksessa, jos mitään poikkeusta ei heitetä, lauseke ja sen arvo tulostetaan System.out-streamiin, jotta näet, mitä jäsennin dekoodasi.

62 kokeile {63 Tupla z; 64 System.out.println ("jäsennetty lauseke:" + res.unparse ()); 65 z = uusi kaksinkertainen (res.value (muuttujat)); 66 System.out.println ("Arvo on:" + z); 67 if (varName! = Null) {68 muuttujaa.put (varName, z); 69 System.out.println ("Määritetty:" + varName); 70} 71} catch (ExecError ee) {72 System.out.println ("Suoritusvirhe" + ee.getMsg () + "!"); 73} 74} 75} 76} 

vuonna STEsimerkki luokka, StreamTokenizer käytetään komento-prosessorin jäsentimessä. Tämän tyyppistä jäsentäjää käytetään yleisesti kuoriohjelmassa tai missä tahansa tilanteessa, jossa käyttäjä antaa komentoja vuorovaikutteisesti. Toinen jäsennin on kapseloitu ParseExpression luokassa. (Katso koko lähde Resurssit-osiosta.) Tämä luokka jäsentää laskimen lausekkeet ja sitä kutsutaan yllä olevalle riville 44. Se on täällä StreamTokenizer edessään jäykin haaste.

Lausekkeen jäsentimen rakentaminen

Laskimen lausekkeiden kielioppi määrittelee muodon "[kohde] operaattori [kohde]" algebrallisen syntaksin. Tämän tyyppinen kielioppi tulee esiin yhä uudelleen ja sitä kutsutaan operaattori kielioppi. Kätevä merkintä käyttäjän kieliopille on:

id ("OPERATOR" id) * 

Yllä oleva koodi olisi "ID-pääte, jota seuraa nolla tai useampia operaattoritunnisteen esiintymiä". StreamTokenizer luokka tuntuu melko ihanteelliselta tällaisten virtojen analysoimiseksi, koska suunnittelu hajottaa luonnollisesti syötevirran sana, määräja tavallinen merkki rahakkeet. Kuten näytän sinulle, tämä on totta pisteeseen asti.

ParseExpression class on suoraviivainen, rekursiivisesti laskeutuva jäsennin lausekkeille heti perustutkinnon suorittaneesta kääntäjän suunnitteluluokasta. Ilmaisu Tämän luokan menetelmä määritellään seuraavasti:

 1 staattinen lausekelauseke (StreamTokenizer st) heittää SyntaxError {2 lauseketuloksen; 3 looginen arvo = väärä; 4 5 tulos = summa (st); 6 taas (! Valmis) {7 kokeile {8 kytkin (st.nextToken ()) 9 tapaus '&': 10 tulos = uusi lauseke (OP_AND, tulos, summa (st)); 11 tauko; 12 tapaus '23} catch (IOException ioe) {24 heittää uusi SyntaxError ("Sai I / O-poikkeuksen."); 25} 26} 27 paluutulosta; 28}