Programování

Lexikální analýza, část 2: Vytvoření aplikace

Minulý měsíc jsem se podíval na třídy, které Java poskytuje k provádění základní lexikální analýzy. Tento měsíc projdu jednoduchou aplikací, která používá StreamTokenizer implementovat interaktivní kalkulačku.

Pro krátký přehled článku z minulého měsíce existují dvě třídy lexikálních analyzátorů, které jsou součástí standardní distribuce Java: StringTokenizer a StreamTokenizer. Tyto analyzátory převádějí svůj vstup na diskrétní tokeny, které může analyzátor použít k pochopení daného vstupu. Analyzátor implementuje gramatiku, která je definována jako jeden nebo více stavů cíle dosažených viděním různých sekvencí tokenů. Když je dosažen stav cíle analyzátoru, provede nějakou akci. Když analyzátor zjistí, že vzhledem k aktuální posloupnosti tokenů neexistují žádné možné stavy cílů, definuje to jako chybový stav. Když analyzátor dosáhne chybového stavu, provede akci obnovy, která analyzátor vrátí zpět do bodu, ve kterém může začít znovu analyzovat. Obvykle se to implementuje konzumací tokenů, dokud se analyzátor nevrátí do platného počátečního bodu.

Minulý měsíc jsem vám ukázal několik metod, které používaly a StringTokenizer analyzovat některé vstupní parametry. Tento měsíc vám ukážu aplikaci, která používá a StreamTokenizer objekt analyzovat vstupní proud a implementovat interaktivní kalkulačku.

Vytváření aplikace

Náš příklad je interaktivní kalkulačka podobná příkazu Unix bc (1). Jak uvidíte, tlačí na StreamTokenizer třídy až na okraj své funkce lexikálního analyzátoru. Slouží tedy jako dobrá ukázka toho, kde lze nakreslit hranici mezi „jednoduchými“ a „složitými“ analyzátory. Tento příklad je aplikace Java, a proto běží nejlépe z příkazového řádku.

Jako rychlý souhrn svých schopností kalkulačka přijímá výrazy ve formě

[název proměnné] "=" výraz 

Název proměnné je volitelný a může to být libovolný řetězec znaků ve výchozím rozsahu slov. (K obnovení paměti těchto znaků můžete použít applet cvičence z článku z minulého měsíce.) Pokud je název proměnné vynechán, hodnota výrazu se jednoduše vytiskne. Pokud je k dispozici název proměnné, je proměnné přiřazena hodnota výrazu. Jakmile jsou proměnné přiřazeny, lze je použít v pozdějších výrazech. Naplňují tedy roli „vzpomínek“ na moderní ruční kalkulačce.

Výraz se skládá z operandů ve formě číselných konstant (konstanty s dvojitou přesností, plovoucí desetinnou čárkou) nebo názvů proměnných, operátorů a závorek pro seskupení konkrétních výpočtů. Legální operátory jsou sčítání (+), odčítání (-), násobení (*), dělení (/), bitové AND (&), bitové OR (|), bitové XOR (#), umocňování (^) a unární negace buď s mínusem (-) pro výsledek komplementu dvou nebo s třeskem (!) pro výsledek komplementu.

Kromě těchto příkazů může naše aplikace kalkulačky také přijímat jeden ze čtyř příkazů: „výpis,“ „vymazání“, „nápověda“ a „ukončení“. The skládka příkaz vytiskne všechny proměnné, které jsou aktuálně definovány, a také jejich hodnoty. The Průhledná příkaz vymaže všechny aktuálně definované proměnné. The Pomoc příkaz vytiskne několik řádků textu nápovědy, aby mohl uživatel začít. The přestat příkaz způsobí ukončení aplikace.

Celá ukázková aplikace se skládá ze dvou analyzátorů - jednoho pro příkazy a příkazy a druhého pro výrazy.

Vytváření analyzátoru příkazů

Analyzátor příkazů je implementován ve třídě aplikace pro příklad STExample.java. (Ukazatel na kód najdete v části Zdroje.) hlavní metoda pro tuto třídu je definována níže. Projdu pro vás jednotlivé kousky.

 1 public static void main (String args []) hodí IOException {2 Hashtable variables = new Hashtable (); 3 StreamTokenizer st = nový StreamTokenizer (System.in); 4 st.eolIsSignificant (true); 5 st.lowerCaseMode (true); 6 st .ordinaryChar ('/'); 7 st .ordinaryChar ('-'); 

V kódu výše první věc, kterou udělám, je přidělit a java.util.Hashtable třída pro uložení proměnných. Poté přidělím a StreamTokenizer a mírně jej upravit z jeho výchozích hodnot. Odůvodnění těchto změn je následující:

  • eolIsSignificant je nastaven na skutečný takže tokenizer vrátí označení konce řádku. Konec řádku používám jako bod, kde končí výraz.

  • lowerCaseMode je nastaven na skutečný takže názvy proměnných budou vždy vráceny malými písmeny. Tímto způsobem názvy proměnných nerozlišují velká a malá písmena.

  • Znak lomítka (/) je nastaven jako běžný znak, takže nebude použit k označení začátku komentáře, a lze jej místo toho použít jako operátor dělení.

  • Znak minus (-) je nastaven jako běžný znak, takže řetězec „3-3“ se segmentuje na tři tokeny - „3“, „-“ a „3“ - nikoli pouze na „3“ a „-3.“ (Nezapomeňte, že analýza čísel je ve výchozím nastavení nastavena na „zapnuto“.)

Jakmile je tokenizer nastaven, analyzátor příkazů běží v nekonečné smyčce (dokud nerozpozná příkaz „quit“, ve kterém okamžiku opustí). Toto je uvedeno níže.

 8 while (true) {9 Expression res; 10 int c = StreamTokenizer.TT_EOL; 11 String varName = null; 12 13 System.out.println ("Zadejte výraz ..."); 14 zkuste {15 while (true) {16 c = st.nextToken (); 17 if (c == StreamTokenizer.TT_EOF) {18 System.exit (1); 19} else if (c == StreamTokenizer.TT_EOL) {20 continue; 21} else if (c == StreamTokenizer.TT_WORD) {22 if (st.sval.compareTo ("dump") == 0) {23 dumpVariables (proměnné); 24 pokračovat; 25} else if (st.sval.compareTo ("clear") == 0) {26 proměnných = nový Hashtable (); 27 pokračovat; 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 pokračovat; 35} 36 varName = st.sval; 37 c = st.nextToken (); 38} 39 zlom; 40} 41 if (c! = '=') {42 hod nový SyntaxError ("chybí počáteční znak '='."); 43} 

Jak vidíte na řádku 16, první token se volá vyvoláním nextToken na StreamTokenizer objekt. Tím se vrátí hodnota označující druh tokenu, který byl naskenován. Návratová hodnota buď bude jednou z definovaných konstant v StreamTokenizer třída, nebo to bude znaková hodnota. Tokeny „meta“ (ty, které nejsou pouze hodnotami znaků), jsou definovány takto:

  • TT_EOF - To znamená, že jste na konci vstupního proudu. Na rozdíl od StringTokenizer, tady není žádný hasMoreTokens metoda.

  • TT_EOL - To vám řekne, že objekt právě prošel sekvencí konce řádku.

  • TT_NUMBER - Tento typ tokenu sděluje kódu analyzátoru, že na vstupu bylo vidět číslo.

  • TT_WORD - Tento typ tokenu označuje, že bylo skenováno celé „slovo“.

Pokud výsledek není jednou z výše uvedených konstant, je to buď hodnota znaku představující znak v „běžném“ rozsahu znaků, který byl naskenován, nebo jeden z nastavených znaků nabídky. (V mém případě není nastaven žádný znak nabídky.) Když je výsledkem jeden z vašich znaků nabídky, citovaný řetězec lze najít v proměnné instance řetězce sval z StreamTokenizer objekt.

Kód v řádcích 17 až 20 se zabývá indikacemi konce řádku a konce souboru, zatímco v řádku 21 je použita klauzule if, pokud byl vrácen token slova. V tomto jednoduchém příkladu je slovo buď příkaz, nebo název proměnné. Řádky 22 až 35 se zabývají čtyřmi možnými příkazy. Pokud je dosažen řádek 36, musí to být název proměnné; následně si program ponechá kopii názvu proměnné a získá další token, který musí být znaménkem rovná se.

Pokud na řádku 41 nebyl token znaménko rovná se, náš jednoduchý parser detekuje chybový stav a vyvolá výjimku, aby jej signalizoval. Vytvořil jsem dvě obecné výjimky, Chyba syntaxe a ExecError, k rozlišení chyb analýzy za běhu od chyb za běhu. The hlavní metoda pokračuje řádkem 44 níže.

44 res = ParseExpression.expression (st); 45} catch (SyntaxError se) {46 res = null; 47 varName = null; 48 System.out.println ("\ n Byla zjištěna chyba syntaxe! -" + se.getMsg ()); 49 while (c! = StreamTokenizer.TT_EOL) 50 c = st.nextToken (); 51 pokračovat; 52} 

V řádku 44 je výraz napravo od znaménka rovná se analyzován s analyzátorem výrazů definovaným v ParseExpression třída. Všimněte si, že řádky 14 až 44 jsou zabaleny v bloku try / catch, který zachycuje chyby syntaxe a řeší je. Když je zjištěna chyba, syntaktickou akcí analyzátoru je spotřebovat všechny tokeny až po další token na konci řádku. To je znázorněno na řádcích 49 a 50 výše.

V tomto okamžiku, pokud nebyla vyvolána výjimka, aplikace úspěšně analyzovala příkaz. Poslední kontrolou je zjistit, že další token je konec řádku. Pokud tomu tak není, chyba zůstala nezjištěna. Nejběžnější chybou budou neodpovídající závorky. Tato kontrola je uvedena v řádcích 53 až 60 níže uvedeného kódu.

53 c = st.nextToken (); 54 if (c! = StreamTokenizer.TT_EOL) {55 if (c == ')') 56 System.out.println ("\ n Byla zjištěna chyba syntaxe! - Mnoho zavíracích parenů."); 57 else 58 System.out.println ("\ nBogusový token na vstupu -" + c); 59 while (c! = StreamTokenizer.TT_EOL) 60 c = st.nextToken (); 61} else { 

Když je dalším tokenem konec řádku, program provede řádky 62 až 69 (viz níže). Tato část metody vyhodnocuje analyzovaný výraz. Pokud byl název proměnné nastaven na řádku 36, je výsledek uložen v tabulce symbolů. V obou případech, pokud není vyvolána žádná výjimka, se výraz a jeho hodnota vytisknou do streamu System.out, abyste viděli, co analyzátor dekódoval.

62 vyzkoušet {63 Double z; 64 System.out.println ("Analyzovaný výraz:" + res.unparse ()); 65 z = nový Double (res.value (variables)); 66 System.out.println ("Hodnota je:" + z); 67 if (varName! = Null) {68 variables.put (varName, z); 69 System.out.println ("Přiřazeno:" + varName); 70} 71} catch (ExecError ee) {72 System.out.println ("Chyba spuštění," + ee.getMsg () + "!"); 73} 74} 75} 76} 

V ST Příklad třída, StreamTokenizer je používán analyzátorem příkazového procesoru. Tento typ syntaktického analyzátoru se běžně používá v shellovém programu nebo v jakékoli situaci, kdy uživatel interaktivně vydává příkazy. Druhý analyzátor je zapouzdřen v ParseExpression třída. (Celý zdroj najdete v části Zdroje.) Tato třída analyzuje výrazy kalkulačky a je vyvolána v řádku 44 výše. Je to tady StreamTokenizer čelí své nejtvrdší výzvě.

Vytváření analyzátoru výrazů

Gramatika výrazů kalkulačky definuje algebraickou syntaxi formuláře „[item] operator [item].“ Tento typ gramatiky se objevuje znovu a znovu a nazývá se operátor gramatika. Pohodlná notace pro gramatiku operátora je:

id („OPERATOR“ id) * 

Výše uvedený kód bude číst „ID terminál následovaný nulovým nebo více výskytem n-tice ID operátora.“ The StreamTokenizer třída se zdá být docela ideální pro analýzu takových proudů, protože design přirozeně rozděluje vstupní proud na slovo, číslo, a obyčejný charakter žetony. Jak vám ukážu, je to pravda až do určité míry.

The ParseExpression class je přímý analyzátor rekurzivního sestupu pro výrazy, přímo z vysokoškolské třídy designu kompilátoru. The Výraz metoda v této třídě je definována takto:

 1 statický výrazový výraz (StreamTokenizer st) vyvolá SyntaxError {2 výsledek výrazu; 3 boolean hotovo = false; 4 5 výsledek = součet (st); 6 while (! Done) {7 try {8 switch (st.nextToken ()) 9 case '&': 10 result = new Expression (OP_AND, result, sum (st)); 11 přestávka; 12 case '23} catch (IOException ioe) {24 throw new SyntaxError ("Got an I / O Exception."); 25} 26} 27 návratový výsledek; 28} 
$config[zx-auto] not found$config[zx-overlay] not found