Mietitkö koskaan kuinka Java-kääntäjä toimii? Täytyykö sinun kirjoittaa jäsentimiä merkintäasiakirjoille, jotka eivät tilaa vakiomuotoja, kuten HTML tai XML? Vai haluatko toteuttaa oman pienen ohjelmointikielesi vain sen helvetin vuoksi? JavaCC
antaa sinun tehdä kaikki tämä Javalla. Joten riippumatta siitä, haluatko vain oppia lisää kääntäjien ja tulkkien toiminnasta tai onko sinulla konkreettisia tavoitteita luoda Java-ohjelmointikielen seuraaja, ole hyvä ja liity kanssani tämän kuukauden tutkimustyöhön JavaCC
, korostettu rakentamalla kätevä pieni komentorivilaskin.
Kääntäjän rakentamisen perusteet
Ohjelmointikielet jaetaan usein keinotekoisesti käännettyihin ja tulkittuihin kieliin, vaikka rajat ovat hämärtyneet. Sellaisena älä huoli siitä. Tässä käsitellyt käsitteet soveltuvat yhtä hyvin sekä käännettyihin että tulkittuihin kieliin. Käytämme sanaa kääntäjä alla, mutta tämän artikkelin soveltamisalaan siinä on sisällytettävä tulkki.
Kääntäjien on suoritettava kolme päätehtävää, kun ne esitetään ohjelmatekstin (lähdekoodin) kanssa:
- Leksikaalinen analyysi
- Syntaktinen analyysi
- Koodin luominen tai suorittaminen
Suurin osa kääntäjän työstä keskittyy vaiheiden 1 ja 2 ympärille, joihin sisältyy ohjelman lähdekoodin ymmärtäminen ja sen syntaktisen oikeellisuuden varmistaminen. Kutsumme tätä prosessia jäsentäminen, kumpi on jäsentäjä 'vastuu.
Leksikaalinen analyysi (lexing)
Leksikaalinen analyysi tarkastelee ohjelman lähdekoodia pinnallisesti ja jakaa sen oikeaan rahakkeet. Tunnus on merkittävä osa ohjelman lähdekoodia. Tunnusesimerkkejä ovat avainsanat, välimerkit, kirjaimet, kuten numerot, ja merkkijonot. Nontokenit sisältävät tyhjän tilan, jota usein ei oteta huomioon, mutta jota käytetään tunnusten erottamiseen, ja kommentit.
Syntaktinen analyysi (jäsentäminen)
Syntaktisen analyysin aikana jäsennin poimii merkityksen ohjelman lähdekoodista varmistamalla ohjelman syntaktisen oikeellisuuden ja rakentamalla ohjelman sisäinen esitys.
Tietokonekieliteoria puhuu ohjelmat,kielioppi, ja Kieli (kielet. Tässä mielessä ohjelma on merkkijono. Kirjaimellinen on tietokonekielen peruselementti, jota ei voida pienentää edelleen. Kielioppi määrittelee säännöt syntaktisesti oikeiden ohjelmien rakentamiseksi. Vain ohjelmat, jotka pelaavat kieliopissa määriteltyjen sääntöjen mukaan, ovat oikeita. Kieli on yksinkertaisesti kaikkien ohjelmien joukko, joka täyttää kaikki kieliopisi säännöt.
Syntaktisen analyysin aikana kääntäjä tutkii ohjelman lähdekoodin kielen kieliopissa määriteltyjen sääntöjen suhteen. Jos jotakin kielioppisääntöä rikotaan, kääntäjä näyttää virheilmoituksen. Matkan varrella, kääntäessään ohjelmaa, kääntäjä luo helposti käsiteltävän sisäisen esityksen tietokoneohjelmasta.
Tietokonekielen kielioppisäännöt voidaan määrittää yksiselitteisesti ja kokonaisuudessaan EBNF (Extended Backus-Naur-Form) -merkinnällä (lisätietoja EBNF: stä, katso Resurssit). EBNF määrittelee kieliopit tuotantosääntöjen perusteella. Tuotantosäännössä todetaan, että kielioppi-elementti - joko literaaleja tai koostettuja elementtejä - voi koostua muista kielioppielementeistä. Literaalit, joita ei voida lukea, ovat staattisen ohjelmatekstin avainsanoja tai fragmentteja, kuten välimerkkejä. Komponentit johdetaan soveltamalla tuotantosääntöjä. Tuotantosäännöillä on seuraava yleinen muoto:
GRAMMAR_ELEMENT: = luettelo kielioppielementeistä | vaihtoehtoinen luettelo kieliopin elementeistä
Tarkastellaan esimerkiksi pienen kielen kielioppisääntöjä, jotka kuvaavat aritmeettisia peruslausekkeita:
lauseke: = numero | lauseke '+' lauseke | expr '-' expr | lauseke '*' lauseke | expr '/' expr | '(' lauseke ')' | - lauseke: = numero + ('.' numero +)? numero: = '0' | "1" | '2' | '3' | '4' | '5' | "6" | '7' | "8" | '9'
Kolme tuotantosääntöä määrittelee kielioppielementit:
lauseke
määrä
numero
Kieliopin määrittelemä kieli antaa meille mahdollisuuden määrittää aritmeettiset lausekkeet. An lauseke
on joko numero tai yksi neljästä infix-operaattorista, joita sovelletaan kahteen lauseke
s, an lauseke
sulkeissa tai negatiivinen lauseke
. A määrä
on liukuluku, jonka desimaaliosa on valinnainen. Määritämme a numero
olla yksi tutuista desimaaliluvuista.
Koodin luominen tai suorittaminen
Kun jäsentäjä jäsentää ohjelman virheettömästi, se on sisäisessä esityksessä, jonka kääntäjä on helppo käsitellä. Nyt on suhteellisen helppoa luoda konekoodi (tai Java-tavukoodi tälle asialle) sisäisestä esityksestä tai suorittaa sisäinen esitys suoraan. Jos teemme edellisen, koomme; jälkimmäisessä tapauksessa puhumme tulkkauksesta.
JavaCC
JavaCC
, saatavana ilmaiseksi, on jäsenningeneraattori. Se tarjoaa Java-kielen laajennuksen ohjelmointikielen kieliopin määrittämiseksi. JavaCC
kehitti alun perin Sun Microsystems, mutta nyt sitä ylläpitää MetaMata. Kuten kaikki kunnolliset ohjelmointityökalut, JavaCC
oli tosiasiallisesti käytetty määrittämään kielioppi JavaCC
syöttömuoto.
Lisäksi, JavaCC
antaa meille mahdollisuuden määritellä kieliopit samalla tavalla kuin EBNF, mikä helpottaa EBNF-kieliopien kääntämistä JavaCC
muoto. Edelleen, JavaCC
on Java: n suosituin jäsenningeneraattori, jossa on joukko ennalta määritettyjä JavaCC
kieliopit käytettävissä käytettäväksi lähtökohtana.
Yksinkertaisen laskimen kehittäminen
Käymme nyt läpi pienen aritmeettisen kielemme rakentaaksemme yksinkertaisen komentorivilaskurin Java-sovelluksessa JavaCC
. Ensin meidän on käännettävä EBNF-kielioppi JavaCC
muodossa ja tallenna se tiedostoon Aritmeettinen.jj
:
vaihtoehdot {LOOKAHEAD = 2; } PARSER_BEGIN (aritmeettinen) julkisen luokan aritmeettinen {} PARSER_END (aritmeettinen) ohitus: "\ t" TOKEN: double expr (): {} term () ("+" expr () double term (): {} "/" termi ()) * double unary (): {} "-" element () double element (): {} "(" lauseke () ")"
Yllä olevan koodin pitäisi antaa sinulle käsitys siitä, miten kielioppi määritetään JavaCC
. vaihtoehtoja
yläosassa oleva osio määrittää joukon vaihtoehtoja kyseiselle kieliopille. Määritämme etsinnän 2. Lisäasetusten hallinta JavaCC
virheenkorjausominaisuudet ja paljon muuta. Nämä vaihtoehdot voidaan vaihtoehtoisesti määrittää JavaCC
komentorivi.
PARSER_BEGIN
lauseke määrittää, että jäsenninluokan määritelmä noudattaa. JavaCC
luo yhden Java-luokan kullekin jäsentäjälle. Kutsumme jäsentäjäluokkaa Aritmeettinen
. Toistaiseksi vaaditaan vain tyhjä luokan määritelmä; JavaCC
lisää jäsentämiseen liittyvät ilmoitukset siihen myöhemmin. Lopetamme luokan määritelmän PARSER_END
lauseke.
OHITA
-osassa yksilöidään merkit, jotka haluamme ohittaa. Meidän tapauksessamme nämä ovat välilyöntihahmoja. Seuraavaksi määritämme kielemme tunnukset TOKEN
-osiossa. Määritämme numerot ja numerot rahakkeiksi. Ota huomioon, että JavaCC
erottaa tunnusten määritelmät ja muiden tuotantosääntöjen määritelmät, mikä eroaa EBNF: stä. OHITA
ja TOKEN
osiot määrittelevät tämän kieliopin leksikaalisen analyysin.
Seuraavaksi määritämme tuotantosäännön lauseke
, ylätason kielioppi-elementti. Huomaa, kuinka määritelmä eroaa merkittävästi määritelmästä lauseke
EBNF: ssä. Mitä tapahtuu? On käynyt ilmi, että yllä oleva EBNF-määritelmä on epäselvä, koska se sallii saman ohjelman useita esityksiä. Tarkastellaan esimerkiksi lauseketta 1+2*3
. Voimme sovittaa 1+2
osaksi lauseke
tuottaa lauseke * 3
, kuten kuvassa 1.
Tai vaihtoehtoisesti voimme ensin ottelu 2*3
osaksi lauseke
johtaen 1 + lauseke
, kuten kuvassa 2 on esitetty.
Kanssa JavaCC
, meidän on määriteltävä kielioppisäännöt yksiselitteisesti. Tämän seurauksena eritellään määritelmä lauseke
kolmeen tuotantosääntöön, jotka määrittelevät kielioppielementit lauseke
, termi
, unary
ja elementti
. Nyt ilmaisu 1+2*3
on jäsennelty kuvan 3 mukaisesti.
Komentoriviltä voimme suorittaa JavaCC
tarkistaa kielioppi:
javacc Arithmetic.jj Java-kääntäjän kääntäjän versio 1.1 (jäsentimen luonti) Tekijänoikeudet (c) 1996-1999 Sun Microsystems, Inc. Tekijänoikeudet (c) 1997-1999 Metamata, Inc. (kirjoita "javacc" ilman argumentteja apua varten) Lukeminen tiedostosta Aritmeettinen.jj. . . Varoitus: Hakusenttien riittävyyden tarkistusta ei suoriteta, koska vaihtoehto LOOKAHEAD on yli 1. Aseta vaihtoehdon FORCE_LA_CHECK arvoksi true to force. Parseri luotu 0 virheellä ja yhdellä varoituksella.
Seuraava tarkistaa kieliopin määrittelyn ongelmien varalta ja luo joukon Java-lähdetiedostoja:
TokenMgrError.java ParseException.java Token.java ASCII_CharStream.java Arithmetic.java ArithmeticConstants.java ArithmeticTokenManager.java
Yhdessä nämä tiedostot toteuttavat jäsentimen Java-sovelluksessa. Voit kutsua tämän jäsentimen alustamalla Aritmeettinen
luokka:
public class Arithmetic toteuttaa ArithmeticConstants {public Arithmetic (java.io.InputStream stream) {...} public Arithmetic (java.io.Reader stream) {...} public Arithmetic (ArithmeticTokenManager tm) {...} staattinen lopullinen julkinen double expr () heittää ParseException {...} staattinen lopullinen julkinen double term () heittää ParseException {...} staattinen final public double unary () heittää ParseException {...} staattinen final public double element () heittää ParseException {. ..} static public void ReInit (java.io.InputStream stream) {...} static public void ReInit (java.io.Reader stream) {...} public void ReInit (ArithmeticTokenManager tm) {...} staattinen final public token getNextToken () {...} static final public token getToken (int index) {...} static final public ParseException generParseException () {...} static final public void enable_tracing () {...} static lopullinen julkinen void disable_tracing () {...}}
Jos haluat käyttää tätä jäsentäjää, sinun on luotava ilmentymä käyttämällä yhtä konstruktoreista. Rakentajien avulla voit kulkea joko InputStream
, a Lukija
tai ArithmeticTokenManager
ohjelman lähdekoodin lähteenä. Seuraavaksi määrität kielesi pääkielioppielementin, esimerkiksi:
Aritmeettinen jäsennin = uusi aritmeettinen (System.in); parser.expr ();
Mitään ei kuitenkaan vielä tapahdu, koska Aritmeettinen.jj
olemme määrittäneet vain kielioppisäännöt. Emme ole vielä lisänneet laskelmien suorittamiseen tarvittavaa koodia. Tätä varten lisäämme asianmukaiset toiminnot kielioppisääntöihin. Calcualtor.jj
sisältää täydellisen laskimen, mukaan lukien toiminnot:
vaihtoehdot {LOOKAHEAD = 2; } PARSER_BEGIN (Laskin) public class Calculator {public static void main (String args []) heittää ParseException {Calculator parser = uusi laskin (System.in); while (true) {parser.parseOneLine (); }}} PARSER_END (Laskin) OHITUS: "\ t" TOKEN: void parseOneLine (): {double a; } {a = lauseke () {System.out.println (a); } | | {System.exit (-1); }} double expr (): {double a; kaksinkertainen b; } {a = termi () ("+" b = lauseke () {a + = b;} | "-" b = lauseke () {a - = b;}) * {palauta a; }} kaksoistermi (): {kaksinkertainen a; kaksinkertainen b; } {a = unary () ("*" b = termi () {a * = b;} | "/" b = termi () {a / = b;}) * {palauta a; }} kaksinkertainen unary (): {double a; } {"-" a = elementti () {return -a; } | a = elementti () {palauttaa a; }} kaksoiselementti (): {Tunnus t; kaksinkertainen a; } {t = {palauta Double.parseDouble (t.toString ()); } | "(" a = lauseke () ")" {palauta a; }}
Päämenetelmä välittää ensin jäsenninobjektin, joka lukee vakiotulosta ja kutsuu sitten parseOneLine ()
loputtomassa silmukassa. Menetelmä parseOneLine ()
itse määritellään ylimääräisellä kielioppisäännöllä. Tämä sääntö yksinkertaisesti määrittelee, että odotamme jokaisen rivin lausekkeen itsestään, että on OK kirjoittaa tyhjiä rivejä ja että lopetamme ohjelman, kun pääsemme tiedoston loppuun.
Olemme muuttaneet alkuperäisten kielioppielementtien palautustyypin palatuksi kaksinkertainen
. Suoritamme asianmukaiset laskelmat siellä, missä ne jäsennetään ja välitetään laskutulokset puhelupuuhun. Olemme myös muuttaneet kielioppielementtien määritelmiä tallentaaksemme tulokset paikallisiin muuttujiin. Esimerkiksi, a = elementti ()
jäsentää elementti
ja tallentaa tuloksen muuttujaan a
. Tämä antaa meille mahdollisuuden käyttää jäsennettyjen elementtien tuloksia oikeanpuoleisen toiminnan koodissa. Toiminnot ovat Java-koodilohkoja, jotka suoritetaan, kun siihen liittyvä kielioppisääntö on löytänyt vastaavuuden syötevirrasta.
Huomaa, kuinka vähän Java-koodia lisäsimme, jotta laskin olisi täysin toimiva. Lisäksi lisätoimintojen, kuten sisäänrakennettujen toimintojen tai jopa muuttujien, lisääminen on helppoa.