Programování

Vytvořte si své vlastní jazyky pomocí JavaCC

Přemýšleli jste někdy, jak funguje kompilátor Java? Potřebujete psát analyzátory pro značkovací dokumenty, které se nepřihlásí k odběru standardních formátů, jako je HTML nebo XML? Nebo chcete implementovat svůj vlastní malý programovací jazyk jen pro to? JavaCC umožňuje to všechno v Javě. Takže ať už máte zájem dozvědět se více o tom, jak fungují překladače a tlumočníci, nebo máte konkrétní ambice vytvořit nástupce programovacího jazyka Java, připojte se ke mně tento měsíc na průzkumu JavaCC, zvýrazněno konstrukcí praktické malé kalkulačky příkazového řádku.

Základy konstrukce překladače

Programovací jazyky jsou často rozděleny, poněkud uměle, do kompilovaných a interpretovaných jazyků, i když hranice se stírají. Nedělejte si s tím starosti. Pojmy zde diskutované platí stejně dobře pro kompilované i interpretované jazyky. Použijeme slovo překladač níže, ale pro účely tohoto článku to bude zahrnovat význam tlumočník.

Překladatelé musí při předložení textu programu (zdrojového kódu) provést tři hlavní úkoly:

  1. Lexikální analýza
  2. Syntaktická analýza
  3. Generování nebo provádění kódu

Převážná část práce překladače se soustředí na kroky 1 a 2, které zahrnují pochopení zdrojového kódu programu a zajištění jeho syntaktické správnosti. Tomuto procesu říkáme analýza, který je analyzátor 'odpovědnost.

Lexikální analýza (lexing)

Lexikální analýza zběžně pohlédne na zdrojový kód programu a rozdělí jej na správný žetony. Token je významná část zdrojového kódu programu. Příklady tokenů zahrnují klíčová slova, interpunkci, literály, jako jsou čísla, a řetězce. Neoprávnění zahrnují prázdné znaky, které se často ignorují, ale používají se k oddělení tokenů, a komentáře.

Syntaktická analýza (analýza)

Během syntaktické analýzy analyzátor extrahuje význam ze zdrojového kódu programu zajištěním syntaktické správnosti programu a vytvořením interní reprezentace programu.

Teorie počítačového jazyka hovoří o programy,gramatika, a jazyky. V tomto smyslu je program posloupností tokenů. Literál je základní prvek počítačového jazyka, který nelze dále redukovat. Gramatika definuje pravidla pro vytváření syntakticky správných programů. Správné jsou pouze programy, které hrají podle pravidel definovaných v gramatice. Jazyk je jednoduše sada všech programů, které splňují všechna vaše gramatická pravidla.

Během syntaktické analýzy kompilátor zkoumá zdrojový kód programu s ohledem na pravidla definovaná v gramatice jazyka. Pokud dojde k porušení některého pravidla gramatiky, kompilátor zobrazí chybovou zprávu. Podél cesty, při zkoumání programu, kompilátor vytvoří snadno zpracovatelnou interní reprezentaci počítačového programu.

Pravidla gramatiky počítačového jazyka lze specifikovat jednoznačně a v celém rozsahu pomocí notace EBNF (Extended Backus-Naur-Form) (více o EBNF viz Zdroje). EBNF definuje gramatiky z hlediska produkčních pravidel. Produkční pravidlo uvádí, že gramatický prvek - buď literály nebo složené prvky - může být složen z dalších gramatických prvků. Literály, které jsou neredukovatelné, jsou klíčová slova nebo fragmenty statického textu programu, například interpunkční symboly. Složené prvky jsou odvozeny použitím produkčních pravidel. Pravidla produkce mají následující obecný formát:

GRAMMAR_ELEMENT: = seznam gramatických prvků | alternativní seznam gramatických prvků 

Jako příklad se podívejme na pravidla gramatiky pro malý jazyk, který popisuje základní aritmetické výrazy:

expr: = číslo | expr '+' expr | expr '-' expr | expr '*' expr | expr '/' expr | '(' expr ')' | - expr number: = digit + ('.' digit +)? číslice: = '0' | „1“ | „2“ | '3' | '4' | '5' | '6' | '7' | '8' | '9' 

Gramatické prvky definují tři pravidla produkce:

  • expr
  • číslo
  • číslice

Jazyk definovaný touto gramatikou nám umožňuje určit aritmetické výrazy. An expr je číslo nebo jeden ze čtyř operátorů infix aplikovaných na dva exprs, an expr v závorkách nebo záporné expr. A číslo je číslo s plovoucí desetinnou čárkou s volitelným desetinným zlomkem. Definujeme a číslice být jednou ze známých desetinných číslic.

Generování nebo provádění kódu

Jakmile analyzátor úspěšně analyzuje program bez chyby, existuje v interní reprezentaci, kterou kompilátor snadno zpracuje. Nyní je relativně snadné vygenerovat strojový kód (nebo Java bytecode) z interní reprezentace nebo přímo provést interní reprezentaci. Pokud uděláme první, sestavujeme; v druhém případě mluvíme o tlumočení.

JavaCC

JavaCC, který je k dispozici zdarma, je generátor syntaktického analyzátoru. Poskytuje rozšíření jazyka Java pro určení gramatiky programovacího jazyka. JavaCC byl původně vyvinut společností Sun Microsystems, ale nyní je udržován společností MetaMata. Jako každý slušný programovací nástroj, JavaCC byl ve skutečnosti použit k určení gramatiky JavaCC vstupní formát.

Navíc, JavaCC umožňuje nám definovat gramatiky podobným způsobem jako EBNF, což usnadňuje překlad gramatik EBNF do JavaCC formát. Dále, JavaCC je nejoblíbenější generátor syntaktických analyzátorů pro Javu s řadou předdefinovaných JavaCC gramatiky, které lze použít jako výchozí bod.

Vývoj jednoduché kalkulačky

Nyní se vrátíme k našemu malému aritmetickému jazyku a vytvoříme jednoduchou kalkulačku příkazového řádku v Javě pomocí JavaCC. Nejprve musíme přeložit gramatiku EBNF do JavaCC formát a uložte jej do souboru Arithmetic.jj:

možnosti {LOOKAHEAD = 2; } PARSER_BEGIN (aritmetika) veřejná třída aritmetika {} PARSER_END (aritmetika) SKIP: "\ t" TOKEN: double expr (): {} term () ("+" expr () double term (): {} "/" výraz ()) * double unary (): {} "-" element () double element (): {} "(" expr () ")" 

Výše uvedený kód by vám měl poskytnout představu o tom, jak určit gramatiku pro JavaCC. The možnosti část v horní části určuje sadu možností pro danou gramatiku. Specifikujeme hledisko 2. Ovládání dalších možností JavaCCfunkce ladění a další. Tyto možnosti lze alternativně specifikovat na JavaCC příkazový řádek.

The PARSER_BEGIN klauzule určuje, že následuje definice třídy analyzátoru. JavaCC generuje pro každý analyzátor jednu třídu Java. Říkáme třídě analyzátoru Aritmetický. Prozatím vyžadujeme pouze definici prázdné třídy; JavaCC později k ní přidá prohlášení související s analýzou. Definici třídy ukončíme znakem PARSER_END doložka.

The PŘESKOČIT část označuje znaky, které chceme přeskočit. V našem případě se jedná o prázdné znaky. Dále definujeme tokeny našeho jazyka v ŽETON sekce. Definujeme čísla a číslice jako tokeny. Všimněte si, že JavaCC rozlišuje mezi definicemi tokenů a definicemi pro další produkční pravidla, která se liší od EBNF. The PŘESKOČIT a ŽETON oddíly specifikují lexikální analýzu této gramatiky.

Dále definujeme produkční pravidlo pro expr, prvek gramatiky nejvyšší úrovně. Všimněte si, jak se tato definice výrazně liší od definice expr v EBNF. Co se děje? Ukázalo se, že výše uvedená definice EBNF je nejednoznačná, protože umožňuje více reprezentací stejného programu. Prozkoumejme například výraz 1+2*3. Můžeme odpovídat 1+2 do expr poddajný expr * 3, jako na obrázku 1.

Nebo bychom mohli nejprve zápasit 2*3 do expr což má za následek 1 + expr, jak je znázorněno na obrázku 2.

S JavaCC, musíme jednoznačně specifikovat pravidla gramatiky. Ve výsledku prolomíme definici expr do tří produkčních pravidel, definujících gramatické prvky expr, období, unární, a živel. Nyní výraz 1+2*3 je analyzován, jak je znázorněno na obrázku 3.

Z příkazového řádku můžeme spustit JavaCC zkontrolovat naši gramatiku:

javacc Arithmetic.jj Java Compiler Kompilátor verze 1.1 (generátor analyzátoru) Copyright (c) 1996-1999 Sun Microsystems, Inc. Copyright (c) 1997-1999 Metamata, Inc. (zadejte „javacc“ bez argumentů pro pomoc) Čtení ze souboru Arithmetic.jj. . . Varování: Kontrola přiměřenosti Lookahead se neprovádí, protože volba LOOKAHEAD je více než 1. Možnost FORCE_LA_CHECK nastavte na hodnotu true, aby se vynutila kontrola. Analyzátor vygenerovaný s 0 chybami a 1 varováními. 

Následující kontroluje problémy s naší definicí gramatiky a generuje sadu zdrojových souborů Java:

TokenMgrError.java ParseException.java Token.java ASCII_CharStream.java Arithmetic.java ArithmeticConstants.java ArithmeticTokenManager.java 

Společně tyto soubory implementují analyzátor v Javě. Tento analyzátor můžete vyvolat vytvořením instance instance Aritmetický třída:

public class Arithmetic implements ArithmeticConstants {public Arithmetic (java.io.InputStream stream) {...} public Arithmetic (java.io.Reader stream) {...} public Arithmetic (ArithmeticTokenManager tm) {...} statická konečná veřejná double expr () vyvolá ParseException {...} statické konečné veřejné double term () vyvolá ParseException {...} statické konečné veřejné double unary () hodí ParseException {...} statické konečné veřejné double element () hodí ParseException {. ..} static public void ReInit (stream java.io.InputStream) {...} static public void ReInit (stream java.io.Reader) {...} public void ReInit (ArithmeticTokenManager tm) {...} static konečný veřejný token getNextToken () {...} statický konečný veřejný token getToken (int index) {...} statický konečný veřejný ParseException generateParseException () {...} statický konečný veřejný void enable_tracing () {...} statický final public void disable_tracing () {...}} 

Pokud jste chtěli použít tento analyzátor, musíte vytvořit instanci pomocí jednoho z konstruktorů. Konstruktory vám umožňují předat buď InputStream, a Čtenář, nebo ArithmeticTokenManager jako zdroj zdrojového kódu programu. Dále určíte hlavní gramatický prvek vašeho jazyka, například:

Aritmetický parser = nový aritmetický (System.in); parser.expr (); 

Zatím se však nic moc neděje, protože v Arithmetic.jj definovali jsme pouze pravidla gramatiky. Ještě jsme nepřidali kód potřebný k provedení výpočtů. K tomu přidáme příslušné akce do pravidel gramatiky. Calcualtor.jj obsahuje kompletní kalkulačku, včetně akcí:

možnosti {LOOKAHEAD = 2; } PARSER_BEGIN (Calculator) public class Calculator {public static void main (String args []) throws ParseException {Calculator parser = new Calculator (System.in); while (true) {parser.parseOneLine (); }}} PARSER_END (kalkulačka) SKIP: "\ t" TOKEN: void parseOneLine (): {double a; } {a = expr () {System.out.println (a); } | | {System.exit (-1); }} double expr (): {double a; dvojité b; } {a = term () ("+" b = expr () {a + = b;} | "-" b = expr () {a - = b;}) * {vrátit a; }} double term (): {double a; dvojité b; } {a = unary () ("*" b = term () {a * = b;} | "/" b = term () {a / = b;}) * {return a; }} double unary (): {double a; } {"-" a = element () {návrat -a; } | a = element () {return a; }} dvojitý prvek (): {Token t; dvojité a; } {t = {návrat Double.parseDouble (t.toString ()); } | "(" a = expr () ")" {vrátit a; }} 

Hlavní metoda nejprve vytvoří instanci objektu analyzátoru, který čte ze standardního vstupu a poté volá parseOneLine () v nekonečné smyčce. Metoda parseOneLine () sám je definován dalším pravidlem gramatiky. Toto pravidlo jednoduše definuje, že očekáváme každý výraz na řádku sám o sobě, že je v pořádku zadávat prázdné řádky a že program ukončíme, pokud se dostaneme na konec souboru.

Změnili jsme návratový typ původních gramatických prvků na návrat dvojnásobek. Provádíme příslušné výpočty přímo tam, kde je analyzujeme a výsledky výpočtu předáváme do stromu volání. Také jsme transformovali definice gramatických prvků tak, aby ukládaly jejich výsledky do lokálních proměnných. Například, a = prvek () analyzuje a živel a uloží výsledek do proměnné A. To nám umožňuje použít výsledky analyzovaných prvků v kódu akcí na pravé straně. Akce jsou bloky kódu Java, které se spustí, když přidružené pravidlo gramatiky najde shodu ve vstupním proudu.

Vezměte prosím na vědomí, jak málo kódu Java jsme přidali, aby byla kalkulačka plně funkční. Navíc je snadné přidat další funkce, jako jsou vestavěné funkce nebo dokonce proměnné.

$config[zx-auto] not found$config[zx-overlay] not found