Technologie mezijazyků a bajtkódů v interpretrech a překladačích

Dnes
Doba čtení: 54 minut

Sdílet

Dva kolegové pracují na svých dvou počítačích
Autor: Shutterstock
Moderní interpretry i překladače nepřekládají zdrojové kódy v jediném kroku, ale rozdělují celý proces do několika fází. Výsledkem třetí fáze je program reprezentovaný v mezijazyku (mezikódu) popř. v bajtkódu.

Obsah

1. Technologie mezijazyků (mezikódů) a bajtkódů v moderních interpretrech a překladačích

2. Typická sekvence transformací prováděných při překladu zdrojového kódu

3. AST a CST

4. Příklad zobrazení struktury výsledného CST

5. Význam mezijazyka v oblasti moderních překladačů

6. Mezijazyk vs bajtkód

7. Historie vývoje bajtkódu a jeho využití

8. Bajtkód pro zásobníkové vs. registrové virtuální stroje

9. Praktická ukázka rozdílu mezi bajtkódem určeným pro zásobníkový a registrový virtuální stroj

10. Bajtkód virtuálního stroje jazyka Java (JVM)

11. Příklady metod přeložených do bajtkódu JVM

12. Bajtkód využívaný interpretrem jazyka Lua

13. Příklady funkcí přeložených do bajtkódu jazyka Lua

14. Bajtkód využívaný jazykem Python

15. Mezikódy používané v GCC

16. Překlad výpočtu do mezikódu GIMPLE a RTL

17. LLVM IR

18. Překlad výpočtu do LLVM IR

19. Obsah navazujícího článku

20. Odkazy na Internetu

1. Technologie mezijazyků (mezikódů) a bajtkódů v moderních interpretrech a překladačích

V dnešním článku se budeme zabývat technologiemi mezijazyků (někdy se nazývají mezikódy, anglicky pak intermediate representation) a bajtkódů (bytecode). Tyto technologie jsou používány ve většině moderních interpretrů a překladačů, protože v mezijazycích popř. s využitím bajtkódů jsou ukládány výsledky jedné či hned několika fází překladu. Jedná se sice o poněkud skrytou vlastnost překladačů a interpretrů, ovšem může být užitečné ji znát, protože nám umožňuje pochopit, jak vlastně překladače pracují, jaké dokážou provádět optimalizace, zda jsou korektně detekovány ty části zdrojového kódu, které mohou vést k nedefinovanému chování atd. Na tento článek budou navazovat další články, ve kterých se budeme podrobněji zabývat mezijazykem nazvaným LLVM IR (použitý je v rodině překlačů LLVM) a taktéž mezijazyky GIMPLE a RTL, které naopak nalezneme v rodině překladačů GCC (GNU Compiler Collection).

Mezikód

Obrázek 1: Pohled na činnost překladače, který je zde chápán jako černá skříňka, jež transformuje zdrojový kód na strojový kód nebo bajtkód. 

Autor: tisnik, podle licence: Rights Managed

2. Typická sekvence transformací prováděných při překladu zdrojového kódu

Při zpracování zdrojových kódů se postupně provádí jednotlivé dílčí kroky, a to jak v klasických překladačích (C, C++, C3, Go, Rust), tak i v těch programovacích jazycích, které provádí překlad „jen“ do bajtkódu s jeho pozdější interpretací (Python, Lua). Díky rozdělení celého zpracování do několika konfigurovatelných a relativně samostatných kroků je zajištěna velká flexibilita a možnost případného relativně snadného rozšiřování o další syntaktické prvky, existuje možnost použití jediné sady nástrojů více programovacími jazyky (GCC, LLVM), lze přidat podporu pro různé výstupní formáty (typický je překlad do nativního kódu pro různé platformy nebo do WebAssembly atd.), podporu pro speciální filtry apod. (nehledě na to, že každá činnost je založena na odlišné teorii). Celý průběh zpracování vypadá při určitém zjednodušení následovně:

  1. U jazyků typu C a C++ se nejdříve zdrojové kódy transformují s využitím preprocesoru, který expanduje makra, řeší podmínky preprocesoru, vkládání obsahu dalších souborů, expanzi jmen funkcí atd.
  2. Většinou se však na samotném začátku zpracování nachází takzvanýlexer, který postupně načítá jednotlivé znaky ze vstupního řetězce (resp. většinou ze vstupního souboru) a vytváří z nich lexikální tokeny. Teoreticky se pro každý programovací jazyk používá odlišný lexer a samozřejmě je možné v případě potřeby si napsat lexer vlastní. V případě Pythonu můžeme použít standardní modul tokenizer, nebo lze alternativně použít například projekt Pygments, jenž obsahuje lexery pro mnoho dalších programovacích jazyků a navíc je snadno rozšiřitelný pro přidání dalších jazyků.
  3. Výstup z lexeru může procházet libovolným počtem filtrů sloužících pro odstranění nebo (častěji) modifikaci jednotlivých tokenů; ať již jejich typů či přímo textu, který tvoří hodnotu tokenu. Díky existenci filtrů je například možné nechat si zvýraznit vybrané bílé znaky, slova se speciálním významem v komentářích (TODO:, FIX:) apod. Některé lexery obsahují filtr přímo ve svém modulu. V běžných překladačích bývá filtrace omezena na odstranění komentářů atd.
  4. Sekvence tokenů tvoří základ pro syntaktickou analýzu. Nástroj, který syntaktickou analýzu provádí, se většinou nazývá parser a proto se taktéž někdy setkáme s pojmem parsing (tento termín je ovšem chybně používán i v těch případech, kdy se provádí „pouze“ lexikální analýza). Výsledkem činnosti parseru je vhodně zvolená datová struktura, typicky abstraktní syntaktický strom (AST); někdy též strom derivační nebo CST. V případě interpretru jazyka Python vypadá postupné zpracování vstupního zdrojového textu takto: lexer → derivační strom (parse tree) → AST.
  5. Z AST nebo CST se další transformací typicky vytváří buď přímo bajtkód (Lua, Python) nebo struktura v mezikódu nebo mezijazyku. Záleží na konkrétním překladači nebo interpretru, jakou sémantiku bude tento mezijazyk mít. Moderní překladače navíc mají hned několik mezijazyků popř. se provádí několik transformací mezi různými fázemi překladu. Pokud je výsledkem bajtkód, bude použit interpretrem nebo JITem, pokud se jedná o mezikód, pokračuje se dalším krokem.
  6. Mezikód/mezijazyk je přeložen backend překladačem do finálního spustitelného kódu určeného pro konkrétní platformu. Touto fází překladu se ovšem dnes zabývat nebudeme.
Poznámka: díky tomu, že se prakticky veškeré zpracování zdrojových textů odehrává na úrovni tokenů, není nutné, aby byl celý zpracovávaný zdrojový kód (nebo jeho tokenizovaná podoba) uložen v operační paměti. Je tedy možné zpracovávat i velmi rozsáhlé zdrojové texty, a to bez větších nároků na operační paměť. Tento princip je použit například v balíčku go/scanner (jenž byl popsán v seriálu o programovacím jazyku Go), ovšem například v případě Pythonu a jeho standardních modulů s lexerem a parserem ve skutečnosti jsme omezeni dostupnou kapacitou paměti (což v praxi nevadí, kromě extrémních případů).
Mezikód

Obrázek 2: Jak překladače, tak i interpretry ve skutečnosti provádí několik transformací. Ze zdrojového kódu vzniká AST a následně bajtkód nebo mezikód. 

Autor: tisnik, podle licence: Rights Managed

3. AST a CST

Standardní abstraktní syntaktický strom (AST), o kterém jsme se několikrát zmínili v předchozí kapitole, má ovšem jednu poměrně zásadní nevýhodu – v případě, že AST upravíme a necháme si z něho zpětně vygenerovat zdrojový kód, ztratíme veškeré původní formátování, tedy například mezery zapsané ve výrazech, přesné umístění komentářů, v některých případech i dekorátorů atd. Z tohoto důvodu se pro ty nástroje, které musí modifikovat zdrojový kód (nepřímo – přes strom) používají nepatrně odlišné typy stromů, které se nazývají Concrete Syntax Tree neboli zkráceně CST (popř. derivační stromy nebo parse tree).

Tyto stromové datové struktury, jak již ostatně jejich název napovídá, reprezentují syntaktickou strukturu daného bloku programového kódu (což může být konstanta, výraz, příkaz, složený blok, funkce, třída, či celý modul). Díky tomu, že si CST pamatuje původní zápis syntaktických prvků, je umožněna relativně snadná tvorba nástrojů pro refaktoring kódu. Příkladem může být knihovna LibCST, která slouží pro manipulaci s CST v Pythonu.

Poznámka: u běžných překladačů se setkáme spíše s tím, že se do běžných AST přidávají informace o tom, jakému místu ve zdrojovém kódu (jméno souboru+řádek+znak na řádku) odpovídá každý uzel stromu. Ovšem konkrétní způsoby reprezentace se mnohdy značně odlišují.

4. Příklad zobrazení struktury výsledného CST

Ukažme si na několika jednoduchých příkladech, jaké informace jsou vlastně v CST uloženy. Začneme výrazem, který obsahuje dva operandy, jeden operátor a mezery:

6 * 7

Výsledný CST lze reprezentovat například následujícím způsobem:

BinaryOperation(
    left=Integer(
        value='6',
        lpar=[],
        rpar=[],
    ),
    operator=Multiply(
        whitespace_before=SimpleWhitespace(
            value=' ',
        ),
        whitespace_after=SimpleWhitespace(
            value=' ',
        ),
    ),
    right=Integer(
        value='7',
        lpar=[],
        rpar=[],
    ),
    lpar=[],
    rpar=[],
)

Výraz bez mezer, ovšem uložený do kulatých závorek:

(6*7)

Výsledný CST:

BinaryOperation(
    left=Integer(
        value='6',
        lpar=[],
        rpar=[],
    ),
    operator=Multiply(
        whitespace_before=SimpleWhitespace(
            value='',
        ),
        whitespace_after=SimpleWhitespace(
            value='',
        ),
    ),
    right=Integer(
        value='7',
        lpar=[],
        rpar=[],
    ),
    lpar=[
        LeftParen(
            whitespace_after=SimpleWhitespace(
                value='',
            ),
        ),
    ],
    rpar=[
        RightParen(
            whitespace_before=SimpleWhitespace(
                value='',
            ),
        ),
    ],
)

Složitější výraz s několika operátory s různou prioritou (** je v Pythonu operátorem umocnění):

(1+2)*3/2**4

Výsledný CST:

BinaryOperation(
    left=BinaryOperation(
        left=BinaryOperation(
            left=Integer(
                value='1',
                lpar=[],
                rpar=[],
            ),
            operator=Add(
                whitespace_before=SimpleWhitespace(
                    value='',
                ),
                whitespace_after=SimpleWhitespace(
                    value='',
                ),
            ),
            right=Integer(
                value='2',
                lpar=[],
                rpar=[],
            ),
            lpar=[
                LeftParen(
                    whitespace_after=SimpleWhitespace(
                        value='',
                    ),
                ),
            ],
            rpar=[
                RightParen(
                    whitespace_before=SimpleWhitespace(
                        value='',
                    ),
                ),
            ],
        ),
        operator=Multiply(
            whitespace_before=SimpleWhitespace(
                value='',
            ),
            whitespace_after=SimpleWhitespace(
                value='',
            ),
        ),
        right=Integer(
            value='3',
            lpar=[],
            rpar=[],
        ),
        lpar=[],
        rpar=[],
    ),
    operator=Divide(
        whitespace_before=SimpleWhitespace(
            value='',
        ),
        whitespace_after=SimpleWhitespace(
            value='',
        ),
    ),
    right=BinaryOperation(
        left=Integer(
            value='2',
            lpar=[],
            rpar=[],
        ),
        operator=Power(
            whitespace_before=SimpleWhitespace(
                value='',
            ),
            whitespace_after=SimpleWhitespace(
                value='',
            ),
        ),
        right=Integer(
            value='4',
            lpar=[],
            rpar=[],
        ),
        lpar=[],
        rpar=[],
    ),
    lpar=[],
    rpar=[],
)

5. Význam mezijazyka v oblasti moderních překladačů

Moderní překladače se v některých ohledech liší od překladačů, které byly používány v šedesátých, sedmdesátých či osmdesátých letech minulého století. Původně byly překladače do značné míry specializované, protože podporovaly pouze jediný programovací jazyk a současně (většinou) produkovaly strojový kód určený pro jedinou platformu (CPU+operační systém). Taktéž optimalizace, pokud byly vůbec prováděny, byly z dnešního pohledu poměrně primitivní (výjimkou jsou ovšem překladače FORTRANu pro superpočítače). V současnosti je ovšem situace mnohem komplikovanější, protože se od překladačů očekává podpora většího množství programovacích jazyků nebo jeho dialektů, podpora pro více architektur CPU, ale například i to, že překladač bude provádět agresivní optimalizace, autovektorizaci kódu atd.

Ukazuje se, že tyto požadavky lze splnit rozdělením překladu na „přední“ a „zadní“ fázi. V první fázi je použit překladač konkrétního programovacího jazyka/jazyků (například Clang). Tento překladač vyprodukuje univerzální mezikód. Ten je následně optimalizován (mnohdy v několika průchodech) a výsledek je poslán do zadního překladače, který provede překlad na konkrétní platformu (CPU+operační systém) s provedením optimalizací použitelných právě pro tuto platformu. Mezijazyk, i když je pro mnoho vývojářů skrytý, je tak ústředním prvkem celého procesu. Mezikód resp. mezijazyk ovšem do značné míry ovlivňuje vlastnosti překladače (například to, do jaké míry dokáže pracovat s nedefinovaným chováním).

Mezikód

Obrázek 3: Některé moderní překladače provádí hned několik fází transformace mezikódu. 

Autor: tisnik, podle licence: Rights Managed

6. Mezijazyk vs bajtkód

Pojem mezijazyk nebo též mezikód je většinou používán v souvislosti se sofistikovanými překladači, které překlad provádí v několika fázích zmíněných v předchozím textu. Naproti tomu mnohé interpretry sice taktéž provádí překlad, ale výsledek se namísto mezijazyk/mezikód nazývá bajtkód. Po technologické stránce se mezijazyky a bajtkódy mohou do značné míry podobat, takže mnohdy význam obou termínů splývá. Typicky obě technologie musí dokázat reprezentovat sekvenci více či méně abstraktních instrukcí a navíc je nutné ukládat i metainformace (jména souborů, čísla řádků, přístupová práva k funkcím a metodám atd.). V operační paměti se v obou případech jedná o „netextové“ struktury. Samotný bajtkód je uložen do binárních souborů (mezikód se naproti tomu většinou na disk neukládá).

Nejprve se ve stručnosti seznámíme s historií používání bajtkódu, která je poměrně dlouhá a přitom i z programátorského hlediska zajímavá, protože úspěch mnoha programovacích jazyků vycházel mj. i z toho, že se díky použití bajtkódů (a s nimi souvisejících virtuálních strojů) mohly jejich překladače snadno a relativně rychle rozšiřovat na různé platformy a architektury počítačů (a to se v žádném případě netýká pouze Javy, jejíž bajtkód se rozšířil na prakticky všechny architektury, ale dříve například i Pascalu či BPCL, což je pradávný předchůdce rodiny jazyků C a C++).

Bajtkód je využíván především dvěma nástroji. Na jedné straně se jedná o překladač (například v případě Javy to může být překladač javac, API tohoto překladače dostupné v moderních JDK či interní překladač využívaný nějakým integrovaným vývojovým prostředím), který překládá zdrojový kód napsaný programátorem (či poloautomaticky vygenerovaný v IDE :-) do bajtkódu; což mj. znamená, že je překlad zcela nezávislý na použité platformě. Na straně druhé již jednou přeložený bajtkód využívá interpret či dnes již v mnoha případech spíše JIT (just in time) překladač. Ve skutečnosti však s bajtkódem může pracovat i mnoho dalších nástrojů. Příkladem ve světě Javy a bajtkódu JVM mohou být například nástroje typu Cobertura a EMMA sloužící pro zjištění, které části zdrojového kódu aplikací jsou pokryty (jednotkovými) testy. Tyto nástroje musí umět dobře kooperovat s virtuálním strojem Javy, proto nejprve modifikují bajtkódy testovaných tříd, aby bylo možné při běhu testů dynamicky zjistit, které řádky kódu jsou skutečně z testů volány.

Podobným způsobem je bajtkód modifikován nástroji podporujícími aspektově orientované programování (aspect oriented programming), které taktéž mohou zasahovat do bajtkódu vygenerovaného překladačem. Teoreticky je sice možné při použití aspektově orientovaného programování transformovat přímo zdrojové kódy programů (což je použito především v jiných programovacích jazycích), ale transformace bajtkódu je v případě Javy mnohem snazší. Ze stejného důvodu (analýza bajtkódu je jednodušší než analýza zdrojového textu) je bajtkód použit pro statickou analýzu a hledání potenciálních chyb nástrojem FindBugs.

Poznámka: ve skutečnosti je však mnohdy výhodnější, když podobné nástroje pracují s AST a nikoli až s výsledným bajtkódem.

7. Historie vývoje bajtkódu a jeho využití

Myšlenka na využití bajtkódu pro překlad a následnou interpretaci programů je v informatice úspěšně realizována a využívána již poměrně dlouhou dobu. Jedním z prvních známých a rozšířených bajtkódů byl bajtkód nazvaný OPCODE, který byl vyvinut pro potřeby programovacího jazyka BCPL. Jazyk BPCL (Basic Combined Programming Language), jenž byl vytvořen Martinem Richardsem již v roce 1966, je z hlediska historie výpočetní techniky důležitý především proto, že z něho Ken Thompson odvodil nový programovací jazyk nazvaný jednoduše B a z jazyka B se postupně vyvinulo klasické Céčko (nejdříve K&R C, posléze dodnes používané ANSI/ISO C následované C99, C11, C17, C23 a C2Y).

Výše zmíněný bajtkód OPCODE byl založen na instrukcích pracujících se zásobníkem operandů (operand stack, viz další kapitoly) a v práci s daty dostupnými přes indexové registry. Například výraz x / y + z se pro proměnné x, y a z uložené na pozicích 1, 2 a 10 přeložil do bajtkódu OPCODE následujícím způsobem, který se nápadně podobá bajtkódu JVM (což ostatně není náhoda, jak si řekneme dále):

LP   1
LP   2
DIV
LP   10
PLUS

Popularita používání bajtkódu byla úzce spojena i s úspěšností takzvaného p-code, což byl (a ostatně stále ještě je) bajtkód využívaný některými překladači programovacího jazyka Pascal (to se ovšem netýká Turbo Pascalu, který se vydal vlastní značně odlišnou cestou). Díky využití p-code bylo snadnější portovat jak překladač Pascalu na různé platformy, tak i interpret vlastního p-kódu. Tento bajtkód je v současnosti ještě využíván na některých školách při výuce IT; mj. i z tohoto důvodu se jedná o stále živý projekt, o čemž ostatně mohou svědčit i stránky projektu UCSD p-system Virtual Machine dostupné na adrese http://ucsd-psystem-vm.sourceforge.net/.

Dále je na tomto místě vhodné zmínit bajtkód využívaný v ekosystému programovacího jazyka Smalltalk. Jedná se o poměrně vysokoúrovňový bajtkód, který se v některých ohledech přibližuje bajtkódu používaném Pythonem (posílání zpráv je v tomto případě velmi podobné volání virtuálních metod apod.). Mimochodem – velká část bajtkódu Smalltalku je reflexivně dostupná i z vlastního jazyka, viz například http://marianopeck.files.wor­dpress.com/2011/05/screen-shot-2011–05–21-at-6–51–24-pm.png.

8. Bajtkód pro zásobníkové vs. registrové virtuální stroje

Využívání bajtkódů má v současnosti za sebou zhruba čtyřicet let postupného vývoje, takže pravděpodobně nebude příliš překvapující, že za tuto (na IT) poměrně dlouhou dobu bylo navrženo a implementováno mnoho různých řešení virtuálního stroje+bajtkódu, která se od sebe v mnoha ohledech odlišovala, a to jak úrovní abstrakce bajtkódu (nízkoúrovňové instrukce dobře transformovatelné na strojový kód vs. vysokoúrovňové instrukce podporující například polymorfismus využívaný například virtuálním strojem Pythonu), tak i způsobem, jakým jednotlivé instrukce pracovaly s argumenty (resp. s operandy).

Naprostou většinu existujících a v současnosti používaných bajtkódů je možné rozdělit do dvou skupin. V první skupině se nachází bajtkódy zpracovávané virtuálními stroji založenými na zásobníku operandů (operand stack) a ve druhé skupině se nachází bajtkódy využívající sadu registrů (register set) nabízených virtuálním strojem (počet registrů může být buď omezen na několik jednotek až desítek registrů nebo naopak prakticky neomezen, takže se spíše jedná o paměť s izolovanými buňkami). Oba zmíněné přístupy mají své přednosti i zápory a taktéž skalní zastánce i odpůrce, jak je tomu ostatně v IT dobrým zvykem :-)

Bajtkódy a virtuální stroje využívající zásobník operandů a instrukce pro práci s argumenty uloženými na tomto zásobníku většinou obsahují mnoho bezparametrických instrukcí, jejichž operační kódy tak mohou být velmi krátké a typicky bývají uloženy v jediném bajtu (z toho také ostatně označení „bajtkód“ vychází). Interpretace takového bajtkódu bývá velmi jednoduchá a lze ji efektivně provádět i na těch mikroprocesorech, které obsahují velmi malé množství pracovních registrů, z čehož ostatně vyplývá i oblíbenost takto navržených bajtkódů v době osmibitových mikroprocesorů a mikrořadičů (poněkud speciálním případem je jazyk Forth).

Před přibližně dvaceti lety, kdy se ve větší míře začaly rozšiřovat JIT překladače, se předpokládalo, že nové JIT překladače budou mít problémy s překladem instrukcí založených na použití zásobníku operandů do strojového kódu moderních mikroprocesorů (ty mají většinou velkou sadu pracovních registrů), ovšem ukázalo se, že JIT dokáží bez větších problémů pracovat jak se zásobníkovými instrukcemi, tak i s instrukcemi využívajícími sadu pracovních registrů VM (viz například JIT v JVM).

Tím se pomalu dostáváme ke druhému rozšířenému typu bajtkódů. Jedná se o bajtkódy, jejichž instrukce dokážou pracovat s obsahem množiny pracovních registrů zvoleného virtuálního stroje. Délka instrukčního slova i možnosti takto navržených bajtkódů závisí především na počtu těchto pracovních registrů; v moderních VM se setkáme minimálně s použitím šestnácti či 32 registry, což znamená, že mnoho instrukcí má délku minimálně dva bajty, mnohdy i tři či čtyři bajty.

Liší se taktéž počet operandů instrukcí – některé bajtkódy využívají takzvaný dvouadresový kód (používají dva registry – jeden registr zdrojový a druhý registr současně zdrojový i cílový), jiné se zaměřují na tříadresový kód (dva zdrojové registry a jeden registr cílový). Způsob interpretace takto navržených bajtkódů může být problematičtější v případě, že mikroprocesor, na němž interpret běží, obsahuje menší množství fyzických pracovních registrů, ovšem (jak již bylo řečeno v předchozím odstavci), při použití JIT se rozdíly mezi oběma způsoby práce s operandy do značné míry rozostřují.

9. Praktická ukázka rozdílu mezi bajtkódem určeným pro zásobníkový a registrový virtuální stroj

Podívejme se nyní na dvě ukázky, ze kterých bude patrné, jak se může lišit bajtkód založený na zásobníkovém virtuálním stroji od bajtkódu, který je určen pro registrový virtuální stroj. V obou příkladech se má vyhodnotit jednoduchý výraz result = a+b*c-d; pro jednoduchost předpokládejme, že všech pět proměnných je lokálních a současně mají typ celé číslo (integer). Způsob překladu do bajtkódu využívajícího zásobník operandů (konkrétně je v tomto případě použit bajtkód JVM) může vypadat následovně:

              ; 0 je index proměnné a
              ; 1 je index proměnné b
              ; 2 je index proměnné c
              ; 3 je index proměnné d
              ; 4 je index proměnné result
0: iload  0   ; uložení obsahu proměnné a na zásobník
1: iload  1   ; uložení obsahu proměnné b na zásobník
2: iload  2   ; uložení obsahu proměnné c na zásobník
3: imul       ; provedení operace b*c, výsledek je ponechán na zásobníku
4: iadd       ; provedení operace a+(b*c)
5: iload  3   ; uložení obsahu proměnné d na zásobník
6: isub       ; dokončit příkaz a+(b*c)+d
7: istore 4   ; uložení výsledku z TOS (obsah zásobníku operandů) do proměnné result

Příklad kompilace téhož příkazu result = a+b*c-d do bajtkódu využívajícího pracovní registry, konkrétně do bajtkódu využívaného programovacím jazykem Lua:

                               ; 0 je index proměnné a
                               ; 1 je index proměnné b
                               ; 2 je index proměnné c
                               ; 3 je index proměnné d
                               ; 4 je index proměnné result
1       [101]   MUL    4 1 2   ; přímé vynásobení obsahu proměnných b a c
2       [101]   ADD    4 0 4   ; přičíst k obsahu proměnné a mezivýsledek, výsledek uložit do proměnné result
3       [101]   SUB    4 4 3   ; odečíst od mezivýsledku obsah proměnné b, výsledek uložit do proměnné result

Ve druhém případě se nemusely vůbec použít instrukce pro uložení proměnných na zásobník ani pro načtení hodnoty zpět ze zásobníku operandů do proměnné, ovšem na druhou stranu musely mít všechny aritmetické instrukce v instrukčním slovu uloženy i indexy operandů.

Poznámka: bajtkód Pythonu je postaven na zásobníku operandů a překlad stejného výrazu bude proveden následovně:
LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (a, b)  ; uložení dvou proměnných a a b na zásobník
LOAD_FAST_BORROW         2 (c)              ; uložení proměnné c na zásobník
BINARY_OP                5 (*)              ; provedení výpočtu b*c s uložením výsledku
BINARY_OP                0 (+)              ; provedení výpočtu a+b*c s uložením výsledku
LOAD_FAST_BORROW         3 (d)              ; uložení proměnné d na zásobník
BINARY_OP               10 (-)              ; provedení výpočtu a+b*c-d s uložením výsledku
STORE_FAST               4 (result)         ; výsledek je ze zásobníku umístěn do proměnné result

10. Bajtkód virtuálního stroje jazyka Java (JVM)

První typ v současnosti používaného bajtkódu, s nímž se v dnešním článku alespoň ve stručnosti seznámíme, je bajtkód využívaný v JVM (Java Virtual Machine). Virtuální stroj jazyka Java samozřejmě využívá jiný soubor „strojových“ instrukcí, než fyzický mikroprocesor, na němž jsou Javovské programy spouštěny. Dokonce ani mikroprocesory určené pro přímé spouštění bajtkódu – dnes již zpola zapomenuté projekty MicroJava a PicoJava, dokonce i ARM procesory s technologií Jazelle – nedokázaly nativně vykonávat všechny instrukce bajtkódu.

V prvních několika letech existence Javy byly instrukce bajtkódu (tvořící těla jednotlivých metod) v naprosté většině případů pouze interpretovány, a to mnohdy velmi jednoduchým způsobem: v programové smyčce se postupně načítaly kódy jednotlivých instrukcí a následně se pro každou instrukci zavolala nativní funkce, která danou instrukci vykonala, většinou s parametry uloženými na zásobníkovém rámci (stack frame) nebo v zásobníku operandů (bližší popis bude uveden v následujících kapitolách).

Naprogramování naivního interpretru pro bajtkód virtuálního stroje Javy ve skutečnosti není příliš složité, protože samotná instrukční sada je poměrně jednoduchá (resp. bývala jednoduchá). Moderní interpretry, například JamVM, jsou navíc naprogramovány takovým způsobem, aby byl jejich přenos na další procesorové architektury snadný a přitom se bajtkód interpretoval co nejefektivnějším způsobem, ideálně s ručně optimalizovanou vnitřní smyčkou (naprogramovanou v případě ručně prováděných optimalizací většinou v assembleru).

Ovšem i přes veškerou snahu programátorů interpretrů nemůže běh programů napsaných v Javě a spouštěných v JVM s interpretovaným bajtkódem v rychlosti soutěžit s nativními aplikacemi. Právě z tohoto důvodu vznikly takzvané just-in-time (JIT) překladače, které dokážou přeložit buď jen určitou sekvenci instrukcí JVM nebo i celý bajtkód do nativního strojového kódu, který je následně spuštěn. Některé JIT překladače, například stále populární HotSpot původně vyvinutý společností Sun Microsystems, používají adaptivní překlad, v němž je bajtkód analyzován v době běhu, takže má JIT překladač k dispozici mnohem více informací, než při statickém překladu. HotSpot navíc umožňuje pomocí takzvaný sond (probes sledovat, který kód je překládán a jaký typ překladu je přitom použit, to je však již poměrně pokročilá a nepříliš často používaná technologie. Ovšem nezávisle na tom, zda jsou instrukce JVM „pouze“ interpretovány, nebo je nejdříve použit JIT, musí být vždy dodrženy všechny vlastnosti jazyka Java i JVM. JIT například může odstranit kontrolu mezí při indexování polí, ale jen tehdy, když je zřejmé, že nedojde k překročení těchto mezí.

V této kapitole si ve stručnosti připomeneme základní vlastnosti bajtkódu virtuálního stroje Javy. Struktura i vlastnosti JVM jsou přesně popsány ve specifikaci JVM, včetně přesného formátu všech datových typů (znaků, celých čísel, čísel s plovoucí řádovou tečkou). Díky dodržování této specifikace různými výrobci virtuálního stroje Javy je zaručeno, že programy napsané v Javě jsou přenositelné na počítače s mnohdy velmi rozdílnou architekturou: od smartphonů s jednojádrovými mikroprocesory až po superpočítače s velkým množstvím výpočetních uzlů a mnohdy s několika desítkami tisíc procesorových jader. Různé nekompatibility, s nimiž programátoři bojovali především v minulosti (ale samozřejmě i dnes, i když v mnohem menší míře), bývají způsobeny buď chybami v implementaci některého virtuálního stroje (a nedodržení specifikace lze považovat za chybu) či nekompatibilitami ve standardních knihovnách, popř. chybami v nativních knihovnách, které lze z JVM volat.

Specifikace virtuálního stroje Javy popisuje některé jeho části velmi precizně (například se jedná o již zmíněné datové typy či o formát instrukcí nebo o strukturu zásobníkového rámce), ovšem u některých dalších částí jsou popsány jen základní vlastnosti a zůstává pouze na tvůrci konkrétní JVM, jakým způsobem se bude daná část virtuálního stroje ve skutečnosti implementovat. Asi nejtypičtějším příkladem je specifikace haldy (heap), u níž se sice předpokládá využití automatické správy paměti, ale nikde již není řečeno, jaký konkrétní algoritmus správy paměti se má použít (v současnosti se využívá hned několik algoritmů) ani jaký formát má být použit pro ukládání referencí na objekty. Díky tomu bylo možné Javu jednoduše přenést z původní 32bitové architektury jak na 16bitové procesory, tak i na procesory pracující s 64bitovými ukazateli (u nichž lze navíc v případě potřeby využít i komprimované ukazatele na objekty) – to vše bez nutnosti zásahu do zdrojových kódů Javovských programů i bez nutnosti jejich rekompilace.

Virtuální stroj jazyka Java obsahuje instrukce, které pracují s operandy několika datových typů. Na rozdíl od mnoha fyzických procesorů se v případě JVM provádí kontroly, zda jsou operace skutečně aplikovány na správné operandy. Není například možné, aby se operace součtu prováděla s jedním operandem typu int a druhým operandem typu long – takový bajtkód by byl při svém načítání odmítnut a vůbec by nebyl spuštěn (to však jinými slovy znamená, že bajtkód je zbytečně redundantní, zejména v porovnání s bajtkódy jazyků Lua a Python, v tomto ohledu se spíše blíží dále zmíněnému LLVM IR). Zajímavé je, že jen velmi málo instrukcí JVM podporuje práci s datovými typy boolean, byte, short a char. Proměnné a parametry metod těchto typů musí být například před provedením některé aritmetické operace nejprve převedeny na typ int pomocí konverzních instrukcí (těch existuje celkem patnáct).

Většina instrukcí virtuálního stroje Javy pracuje s operandy uloženými na takzvaném zásobníku operandů (operand stack). Zásobník operandů (v tomto případě se již jedná o skutečný zásobník typu LIFO – Last In, First Out) je vytvářen v čase běhu aplikace pro každou zavolanou metodu, což mj. znamená, že je při spuštění metody vždy prázdný (zásobník operandů je podle specifikace součástí zásobníkového rámce, jeho konkrétní umístění však je libovolné). Již v čase překladu zdrojového kódu je pro každou metodu zjištěno, jak velká oblast paměti má být pro zásobník operandů vyhrazena a samozřejmě je prováděna kontrola, zda se v době běhu aplikace tato velikost nepřekročí (to by se nemělo u validního bajtkódu stát).

Poznámka: v tomto ohledu se bajtkód JVM podobá bajtkódu Pythonu.

Virtuální stroj Javy kontroluje typy operandů uložených na zásobník operandů a zajišťuje, že se nad těmito operandy budou provádět pouze typově bezpečné operace. V praxi to například znamená, že není možné na zásobník uložit dvě hodnoty typu float a následně provést instrukci iadd, protože tato instrukce vyžaduje, aby na zásobníku byly uloženy dvě hodnoty typu int (i když floatint mají shodnou bitovou šířku).

11. Příklady metod přeložených do bajtkódu JVM

Základní vlastnosti bajtkódu virtuálního stroje Javy si ukážeme na způsobu překladu testovací třídy obsahující několik statických metod. Tím, že jsou všechny testované metody statické, je zajištěno, že se jim nebude předávat další (ve zdrojovém kódu skrytý) parametr this, jiný významnější rozdíl mezi statickými a nestatickými metodami v bajtkódu nenajdeme.

První dvě metody nop1() a nop2() jsou bezparametrické, mají prázdné tělo a nevrací žádnou hodnotu, takže lze předpokládat, že jejich bajtkód bude velmi jednoduchý:

    /**
     * Prazdna metoda bez parametru.
     */
    public static void nop1() {
    }
 
    /**
     * Taktez prazdna metoda bez parametru.
     */
    public static void nop2() {
        return;
    }

Další metoda, která se jmenuje answer(), je taktéž bezparametrická, ale vrací konstantní celočíselnou hodnotu 42:

    /**
     * Metoda bez parametru vracejici konstantu.
     */
    public static int answer() {
        return 42;
    }

Následuje přetížená metoda add(), jejíž jednotlivé varianty akceptují různé celočíselné i reálné hodnoty; poslední varianta této metody spojuje řetězce (a to stejným operátorem +). Na příkladu metod add() si povšimněte toho, že operace + není pro datové typy byte a short uzavřena (výsledkem je hodnota typu int), což vychází z vlastností bajtkódu. Ostatně ani další aritmetické operace pro tyto datové typy taktéž nejsou uzavřeny:

    /**
     * Soucet dvou celych cisel typu short.
     */
    public static short add(short x, short y) {
        return (short)(x+y);
    }
 
    /**
     * Soucet dvou celych cisel typu int.
     */
    public static int add(int x, int y) {
        return x+y;
    }
 
    /**
     * Soucet dvou celych cisel typu long.
     */
    public static long add(long x, long y) {
        return x+y;
    }
 
    /**
     * Soucet dvou celych cisel typu float.
     */
    public static float add(float x, float y) {
        return x+y;
    }
 
    /**
     * Soucet dvou celych cisel typu double.
     */
    public static double add(double x, double y) {
        return x+y;
    }
 
    /**
     * Spojeni dvou retezcu.
     */
    public static String add(String x, String y) {
        return x+y;
    }

Následuje další testovací metoda pojmenovaná isNegative() obsahující podmíněný příkaz. Ten by zde ve skutečnosti nemusel být uveden, neboť celé tělo metody lze zkrátit do jediného výrazu, jehož výsledek by byl vracen příkazem return, ovšem my potřebujeme zjistit, jakým způsobem se do bajtkódu JVM překládají konstrukce typu if-then-else. Ve zdrojovém kódu metody fibonacciIter() jsou použity jak podmínky, tak i počítaná programová smyčka typu for a v poslední testované metodě fibonacciRecursive() je použita rekurze, tj. metoda přímo volá samu sebe. Podívejme se nyní na celý zdrojový kód testovací třídy::

/**
 * Trida s nekolika jednoduchymi statickymi metodami
 * pro otestovani zakladnich vlastnosti bajtkodu JVM.
 */
public class Test {
 
    /**
     * Prazdna metoda bez parametru.
     */
    public static void nop1() {
    }
 
    /**
     * Taktez prazdna metoda bez parametru.
     */
    public static void nop2() {
        return;
    }
 
    /**
     * Metoda bez parametru vracejici konstantu.
     */
    public static int answer() {
        return 42;
    }
 
    /**
     * Soucet dvou celych cisel typu byte.
     */
    public static byte add(byte x, byte y) {
        return (byte)(x+y);
    }
 
    /**
     * Soucet dvou celych cisel typu short.
     */
    public static short add(short x, short y) {
        return (short)(x+y);
    }
 
    /**
     * Soucet dvou celych cisel typu int.
     */
    public static int add(int x, int y) {
        return x+y;
    }
 
    /**
     * Soucet dvou celych cisel typu long.
     */
    public static long add(long x, long y) {
        return x+y;
    }
 
    /**
     * Soucet dvou celych cisel typu float.
     */
    public static float add(float x, float y) {
        return x+y;
    }
 
    /**
     * Soucet dvou celych cisel typu double.
     */
    public static double add(double x, double y) {
        return x+y;
    }
 
    /**
     * Spojeni dvou retezcu.
     */
    public static String add(String x, String y) {
        return x+y;
    }
 
    /**
     * Metoda s podminkou.
     */
    public static boolean isNegative(int x) {
        if (x < 0) {
            return true;
        }
        return false;
    }
 
    /**
     * Metoda s podminkou a se smyckou.
     */
    public static long fibonacciIter(int n) {
        if (n <= 1) {
            return n;
        }
        long result = 0;
        long n1 = 0;
        long n2 = 1;
        for(n--; n > 0; n--) {
            result = n1 + n2;
            n1 = n2;
            n2 = result;
        }
        return result;
    }
 
    /**
     * Metoda s rekurzi.
     */
    public static long fibonacciRecursive(int n) {
        if (n <= 1) {
            return n;
        }
        else {
            return fibonacciRecursive(n-1) + fibonacciRecursive(n-2);
        }
    }
 
    /**
     * Vse je nutne otestovat.
     */
    public static void main(String[] args) {
        nop1();
        nop2();
        System.out.println(answer());
        System.out.println(add((byte)1, (byte)2));
        System.out.println(add((short)1, (short)2));
        System.out.println(add(1, 2));
        System.out.println(add(1L, 2L));
        System.out.println(add(1f, 2f));
        System.out.println(add(1., 2.));
        System.out.println(add("Hello ", "world!"));
        System.out.println(isNegative(-10));
 
        for (int n=0; n <= 10; n++) {
            System.out.println(n + "\t" + fibonacciIter(n) + "\t" + fibonacciRecursive(n));
        }
 
    }
}

Bajtkód přeložených metod je vypsán pod tímto odstavcem.

V metodě nop1() je použita pouze jediná instrukce return pro návrat z těla metody:

public static void nop1();
  Code:
   0:   return     ; pouze návrat z metody

Totéž platí pro metodu nop2():

public static void nop2();
  Code:
   0:   return     ; pouze návrat z metody

V metodě answer() je s využitím instrukce ireturn vrácena hodnota uložená na vrcholu zásobníku operandů (TOS):

public static int answer();
  Code:
   0:   bipush  42 ; uložit na zásobník konstantu 42
   2:   ireturn    ; výskok z metody s předáním návratové hodnoty

Metoda add() využívá pro typ byte automatický převod hodnotu argumentu či lokální proměnné na typ int, ovšem výsledek se musí zpětně konvertovat:

public static byte add(byte, byte);
  Code:
   0:   iload_0    ; uložit první parametr na zásobník (+ provést konverzi na int)
   1:   iload_1    ; uložit druhý parametr na zásobník (+ provést konverzi na int)
   2:   iadd       ; provést součet
   3:   i2b        ; zpětná konverze na byte
   4:   ireturn    ; výskok z metody s předáním návratové hodnoty

Totéž platí v případě použití celočíselného datového typu short:

public static short add(short, short);
  Code:
   0:   iload_0    ; uložit první parametr na zásobník (+ provést konverzi na int)
   1:   iload_1    ; uložit druhý parametr na zásobník (+ provést konverzi na int)
   2:   iadd       ; provést součet
   3:   i2s        ; zpětná konverze na short
   4:   ireturn    ; výskok z metody s předáním návratové hodnoty

Součet dvou čísel typu int s využitím zásobníku operandů je podporován přímo instrukcí bajtkódu:

public static int add(int, int);
  Code:
   0:   iload_0    ; uložit první parametr na zásobník
   1:   iload_1    ; uložit druhý parametr na zásobník
   2:   iadd       ; provést součet
   3:   ireturn    ; výskok z metody s předáním návratové hodnoty

Součet dvou čísel typu long s využitím zásobníku operandů je taktéž podporován přímo instrukcí bajtkódu:

public static long add(long, long);
  Code:
   0:   lload_0    ; uložit první parametr na zásobník
   1:   lload_2    ; uložit druhý parametr na zásobník
   2:   ladd       ; provést součet
   3:   lreturn    ; výskok z metody s předáním návratové hodnoty

Totéž platí pro argumenty či proměnné typu float:

public static float add(float, float);
  Code:
   0:   fload_0    ; uložit první parametr na zásobník
   1:   fload_1    ; uložit druhý parametr na zásobník
   2:   fadd       ; provést součet
   3:   freturn    ; výskok z metody s předáním návratové hodnoty

Totéž platí pro argumenty či proměnné typu double:

public static double add(double, double);
  Code:
   0:   dload_0    ; uložit první parametr na zásobník
   1:   dload_2
   2:   dadd       ; provést součet
   3:   dreturn    ; výskok z metody s předáním návratové hodnoty

Spojení dvou řetězců je naproti tomu realizováno přes instanci třídy StringBuilder a metody append(). Výsledek je nutné převést na řetězec metodou toString():

public static java.lang.String add(java.lang.String, java.lang.String);
  Code:
                   ; tato lokální proměnná se použije při spojování řetězců
   0:   new     #2; //class java/lang/StringBuilder
   3:   dup
                   ; zavolat konstruktor třídy StringBuilder
   4:   invokespecial   #3; //Method java/lang/StringBuilder."init":()V
                   ; zavolat metodu append() třídy StringBuilder pro první parametr
   7:   aload_0
   8:   invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
                   ; zavolat metodu append() třídy StringBuilder pro druhý parametr
   11:  aload_1
   12:  invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
                   ; zpětný převod na řetězec
   15:  invokevirtual   #5; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   18:  areturn    ; výskok z metody s předáním návratové hodnoty

V metodě isNegative() je použita instrukce pro podmíněný skok ifge. Povšimněte si toho, že pravdivostní hodnoty jsou v bajtkódu reprezentovány celočíselnými konstantami 0 a 1:

public static boolean isNegative(int);
  Code:
   0:   iload_0    ; uložit parametr na zásobník
   1:   ifge    6  ; podmíněný skok na lokální adresu 6 při splnění podmínky
   4:   iconst_1   ; 1==true
   5:   ireturn    ; výskok z metody s předáním návratové hodnoty true
   6:   iconst_0   ; 0==false
   7:   ireturn    ; výskok z metody s předáním návratové hodnoty false

Nejsložitější je (podle očekávání) bajtkód metody fibonacciIter(), v níž je použito několika podmíněných skoků i jednoho skoku nepodmíněného:

public static long fibonacciIter(int);
  Code:
   0:   iload_0    ; uložit první parametr na zásobník
   1:   iconst_1   ; n se bude porovnávat s hodnotou 1
   2:   if_icmpgt 8; podmíněný skok na instrukci na indexu 8
   5:   iload_0    ; pro n menší nebo rovno 1 se vrátí přímo n
   6:   i2l        ; konverze n na long
   7:   lreturn    ; výskok z metody s předáním návratové hodnoty false
 
   8:   lconst_0   ; příprava pro zahájení počítané programové smyčky
   9:   lstore_1
   10:  lconst_0
   11:  lstore_3
   12:  lconst_1
   13:  lstore  5
   15:  iinc    0, -1
 
   18:  iload_0    ; zde začíná programová smyčka
   19:  ifle    39 ; test na ukončení programové smyčky
   22:  lload_3
   23:  lload   5
   25:  ladd
   26:  lstore_1
   27:  lload   5
   29:  lstore_3
   30:  lload_1
   31:  lstore  5
   33:  iinc    0, -1
   36:  goto    18 ; skok na začátek programové smyčky
   39:  lload_1
   40:  lreturn    ; výskok z metody s předáním návratové hodnoty false

Bajtkód metody fibonacciRecursive() poměrně přesně odpovídá své zdrojové předloze:

public static long fibonacciRecursive(int);
  Code:
   0:   iload_0    ; uložit první parametr na zásobník
   1:   iconst_1   ; n se bude porovnávat s hodnotou 1
   2:   if_icmpgt 8; podmíněný skok na instrukci na indexu 8
   5:   iload_0    ; pro n menší nebo rovno 1 se vrátí přímo n
   6:   i2l        ; konverze n na long
   7:   lreturn    ; výskok z metody s předáním návratové hodnoty false
 
   8:   iload_0
   9:   iconst_1
   10:  isub       ; provedení operace n-1
                   ; rekurzivní volání metody
   11:  invokestatic    #6; //Method fibonacciRecursive:(I)J
 
   14:  iload_0
   15:  iconst_2
   16:  isub       ; provedení operace n-2
                   ; rekurzivní volání metody
   17:  invokestatic    #6; //Method fibonacciRecursive:(I)J
 
   20:  ladd       ; sečíst výsledek (návratovou hodnotu) obou rekurzivně volaných metod
   21:  lreturn    ; výskok z metody s předáním návratové hodnoty false

12. Bajtkód využívaný interpretrem jazykem Lua

Dalším bajtkódem, s nímž se v dnešním článku alespoň ve stručnosti seznámíme, je bajtkód využívaný programovacím jazykem Lua. Bajtkód tohoto jazyka se v mnoha ohledech odlišuje od bajtkódu JVM. Pravděpodobně nejnápadnějším rozdílem mezi bajtkódem JVM a bajtkódem jazyka Lua je fakt, že se v Lua VM nepoužívá zásobník operandů, protože indexy operandů jsou přímo součástí instrukčního slova.

I formát instrukčních kódů je od JVM velmi odlišný, protože zatímco v případě bajtkódu JVM je kód instrukce uložen v celém bajtu (s několika málo výjimkami), je u Lua VM kód instrukce uložen v pouhých šesti bitech, zatímco zbylých 26 bitů instrukčního slova je rezervováno pro uložení indexů operandů či konstant. Bytekód Lua VM taktéž obsahuje spíše vysokoúrovňové instrukce, které dobře reflektují vlastnosti tohoto programovacího jazyka. Existují například instrukce pro implementaci programové smyčky for, instrukce pro práci s (asociativními) poli tvořícími nejdůležitější strukturovaný datový typ jazyka Lua a dokonce se v bajtkódu nachází instrukce pro vytvoření uzávěru (closure) a pro tail call.

Instrukce mohou mít jeden z následujících formátů:

iABC

# Označení Délka bitového pole Význam
1 i 6 kód instrukce
2 A 8 index či hodnota prvního operandu
3 B 9 index či hodnota druhého operandu
4 C 9 index či hodnota třetího operandu

iABx

# Označení Délka bitového pole Význam
1 i 6 kód instrukce
2 A 8 index či hodnota prvního operandu
3 Bx 18 index či hodnota druhého operandu

iAsBx

# Označení Délka bitového pole Význam
1 i 6 kód instrukce
2 A 8 index či hodnota prvního operandu
3 sBx 18 index či hodnota druhého operandu (zde se znaménkem)

iAx

# Označení Délka bitového pole Význam
1 i 6 kód instrukce
2 Ax 26 index či hodnota prvního (jediného) operandu

isJ

# Označení Délka bitového pole Význam
1 i 5 kód instrukce
2 sJ 26 cíl skoku
3 k 1 registr nebo konstanta
LLVM

Obrázek 4: Různé instrukce podporované v bajtkódu jazyka Lua. 

Autor: tisnik, podle licence: Rights Managed

13. Příklady funkcí přeložených do bajtkódu jazyka Lua

Pro porovnání bajtkódů JVM a Lua VM vytvoříme sadu funkcí, které se do co největší míry podobají metodám použitým ve třídě Test1.java z předchozích kapitol. Jedním z velkých rozdílů mezi jazyky Java a Lua je dynamická povaha datových typů v Lue, což například znamená, že se původní přetížená metoda add() musí implementovat funkcí add() pro čísla a funkcí addStr() pro řetězce:

--
-- Modul s nekolika jednoduchymi funkcemi
-- pro otestovani zakladnich vlastnosti bajtkodu jazyka Lua
--
 
--
-- Prazdna funkce bez parametru.
--
function nop1()
end
 
--
-- Taktez prazdna funkce bez parametru.
--
function nop2()
    return
end
 
--
-- Funkce bez parametru vracejici konstantu.
--
function answer()
    return 42
end
 
--
-- Soucet dvou cisel.
--
function add(x, y)
    return x+y
end
 
--
-- Spojeni dvou retezcu.
--
function addStr(x, y)
    return x..y
end
 
--
-- Funkce s podminkou.
--
function isNegative(x)
    if x < 0 then
        return true
    end
    return false
end
 
--
-- Funkce s podminkou a se smyckou.
--
function fibonacciIter(n)
    if n <= 1 then
        return n
    end
 
    local result = 0
    local n1 = 0
    local n2 = 1
 
    for i = n-1, 1, -1 do
        result = n1 + n2
        n1 = n2
        n2 = result
    end
 
    return result
end
 
--
-- Funkce s rekurzi.
--
function fibonacciRecursive(n)
    if n <= 1 then
        return n
    else
        return fibonacciRecursive(n-1) + fibonacciRecursive(n-2)
    end
end
 
--
-- Vse je nutne otestovat.
--
function main()
    nop1()
    nop2()
    print(answer())
    print(add(1, 2))
    print(addStr("Hello ", "world!"))
    print(isNegative(-10))
 
    for n = 0, 10 do
        print(n .. "\t" .. fibonacciIter(n) .. "\t" .. fibonacciRecursive(n))
    end
end
 
main()

Podívejme se nyní na vygenerovaný bajtkód:

Funkce nop1() obsahuje pouze jedinou instrukci pro návrat z funkce. První a druhý parametr instrukce RETURN určuje, které registry budou použity pro návratové hodnoty (může jich být více):

function <Test.lua:9,10> (1 instruction at 0x90a7c88)
0 params, 2 slots, 0 upvalues, 0 locals, 0 constants, 0 functions
        1       [10]    RETURN          0 1

U funkce nop2() poněkud překvapivě narazíme na dvojici instrukcí RETURN, protože překladač provádí překlad bez optimalizací (což oceníme při ladění):

function <Test.lua:15,17> (2 instructions at 0x90a7de0)
0 params, 2 slots, 0 upvalues, 0 locals, 0 constants, 0 functions
        1       [16]    RETURN          0 1
        2       [17]    RETURN          0 1

Ve funkci answer() se vrací konstanta 42 a opět zde můžeme vidět automaticky vytvořenou koncovou instrukci RETURN (ve skutečnosti RETURN nil):

function <Test.lua:22,24> (3 instructions at 0x90a7f08)
0 params, 2 slots, 0 upvalues, 0 locals, 1 constant, 0 functions
        1       [23]    LOADK           0 -1    ; 42
        2       [23]    RETURN          0 2
        3       [24]    RETURN          0 1

Součet dvou parametrů předaných funkci add() je velmi jednoduchý – sečtou se hodnoty parametru číslo 0 s hodnotou parametru číslo 1, výsledek se uloží do registru 2, který je následně vrácen první instrukcí RETURN:

function <Test.lua:29,31> (3 instructions at 0x90a7d88)
2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions
        1       [30]    ADD             2 0 1
        2       [30]    RETURN          2 2
        3       [31]    RETURN          0 1

V bajtkódu nalezneme i instrukci pro konkatenaci řetězců:

function <Test.lua:36,38> (5 instructions at 0x90a8308)
2 params, 4 slots, 0 upvalues, 2 locals, 0 constants, 0 functions
        1       [37]    MOVE            2 0
        2       [37]    MOVE            3 1
        3       [37]    CONCAT          2 2 3
        4       [37]    RETURN          2 2
        5       [38]    RETURN          0 1

V bajtkódu funkce isNegative() si povšimněte způsobu načtení pravdivostních konstant do registru 1 i způsobu provedení podmíněného skoku:

function <Test.lua:43,48> (7 instructions at 0x90a8500)
1 param, 2 slots, 0 upvalues, 1 local, 1 constant, 0 functions
        1       [44]    LT              0 0 -1  ; - 0
        2       [44]    JMP             0 2     ; to 5
        3       [45]    LOADBOOL        1 1 0
        4       [45]    RETURN          1 2
        5       [47]    LOADBOOL        1 0 0
        6       [47]    RETURN          1 2
        7       [48]    RETURN          0 1

Bajtkód funkce fibonacciIter() je relativně krátký (zejména v porovnání s jeho variantou pro JVM), protože se zde využívají speciální instrukce FORPREP a FORLOOP, jimiž je implementována programová smyčka typu for:

function <Test.lua:53,69> (16 instructions at 0x90a7fa0)
1 param, 8 slots, 0 upvalues, 8 locals, 3 constants, 0 functions
        1       [54]    LE              0 0 -1  ; - 1
        2       [54]    JMP             0 1     ; to 4
        3       [55]    RETURN          0 2
        4       [58]    LOADK           1 -2    ; 0
        5       [59]    LOADK           2 -2    ; 0
        6       [60]    LOADK           3 -1    ; 1
        7       [62]    SUB             4 0 -1  ; - 1
        8       [62]    LOADK           5 -1    ; 1
        9       [62]    LOADK           6 -3    ; -1
        10      [62]    FORPREP         4 3     ; to 14
        11      [63]    ADD             1 2 3
        12      [64]    MOVE            2 3
        13      [65]    MOVE            3 1
        14      [62]    FORLOOP         4 -4    ; to 11
        15      [68]    RETURN          1 2
        16      [69]    RETURN          0 1

Způsob rekurzivního volání funkce si vyžádá podrobnější vysvětlení, které bude uvedeno příště:

function <Test.lua:74,80> (13 instructions at 0x90a87d8)
1 param, 4 slots, 1 upvalue, 1 local, 3 constants, 0 functions
        1       [75]    LE              0 0 -1  ; - 1
        2       [75]    JMP             0 2     ; to 5
        3       [76]    RETURN          0 2
        4       [76]    JMP             0 8     ; to 13
        5       [78]    GETTABUP        1 0 -2  ; _ENV "fibonacciRecursive"
        6       [78]    SUB             2 0 -1  ; - 1
        7       [78]    CALL            1 2 2
        8       [78]    GETTABUP        2 0 -2  ; _ENV "fibonacciRecursive"
        9       [78]    SUB             3 0 -3  ; - 2
        10      [78]    CALL            2 2 2
        11      [78]    ADD             1 1 2
        12      [78]    RETURN          1 2
        13      [80]    RETURN          0 1

Bajtkód používaný Lua VM je (alespoň podle mého názoru) nejenom čitelnější, ale i kratší, a to mj. i kvůli existenci specializovaných instrukcí pro implementaci programové smyčky for i díky existenci aritmetických a logických instrukcí využívajících tříadresový kód (není potřeba složitě manipulovat s hodnotami ukládanými na zásobník operandů).

14. Bajtkód využívaný jazykem Python

Posledním v současnosti používaným bajtkódem, o němž se v dnešním článku zmíníme, je bajtkód využívaný programovacím jazykem Python, konkrétně jeho původní verzí CPython (kromě tohoto bajtkódu lze najít i další bajtkódy Pythonu určené pro jiné VM, například Mamba atd.). Již v předchozích kapitolách jsme si řekli, že bajtkód JVM je poměrně nízkoúrovňový, zejména v porovnání s bajtkódem používaným v programovacím jazyku Lua (resp. přesněji řečeno virtuálním strojem tohoto jazyka). Totéž platí, a to dokonce ještě ve větší míře, i pro bajtkód jazyka Python. Ten je opět založen na zásobníku operandů, ovšem mnohé instrukce pracující s jedním či dvěma operandy (samozřejmě uloženými na zásobníku) ve skutečnosti mohou volat metody objektů a nikoli pouze provádět operace nad primitivními datovými typy. Platí to především pro všechny „aritmetické“ operace, například i pro operátor +, který se překládá do instrukce BINARY_ADD.

To například znamená, že se jednoduchá funkce add() se dvěma operandy:

def add(x, y):
    return x+y

přeloží do následující čtveřice instrukcí bajtkódu:

add:
 28           0 LOAD_FAST                0 (x)
              3 LOAD_FAST                1 (y)
              6 BINARY_ADD
              7 RETURN_VALUE

Tuto funkci lze ovšem volat jak s číselnými parametry, tak i s řetězci, n-ticemi, seznamy či objekty s implementovanou metodou __add__, takže instrukce BINARY_ADD není zcela porovnatelná například s JVM instrukcemi iadd, ladd atd. operujícími pouze nad konkrétním primitivním datovým typem.:

    print(add(1, 2))
    print(add(1., 2))
    print(add("Hello ", "world!"))
    print(add([1,2,3], [4,5,6]))
    print(add((1,2,3), (4,5,6)))

Kromě toho může bajtkód Pythonu obsahovat i instrukce pro snazší tvorbu smyček (BREAK_LOOP, CONTINUE_LOOP) i pro práci s kolekcemi (LIST_APPEND, MAP_ADD, BUILD_SLICE apod).

Podobně jako tomu bylo v případě Javy i programovacího jazyka Lua, i pro Python byla vytvořena demonstrační aplikace s funkcemi, které se snaží co nejvíce přiblížit původním zdrojovým kódům v Javě a Lue (právě proto zde nejsou použity některé pro Python typické idiomy). Podívejme se nejdříve na zdrojový kód tohoto příkladu, který ve své závěrečné části obsahuje funkci pro disassembling bajtkódu:

#
# Modul s nekolika jednoduchymi funkcemi
# pro otestovani zakladnich vlastnosti bajtkodu jazyka Python
#
 
#
# Prazdna funkce bez parametru.
#
def nop1():
    pass
 
#
# Taktez prazdna funkce bez parametru.
#
def nop2():
    return
 
#
# Funkce bez parametru vracejici konstantu.
#
def answer():
    return 42
 
#
# Soucet dvou cisel.
#
def add(x, y):
    return x+y
 
#
# Funkce s podminkou.
#
def isNegative(x):
    if x < 0:
        return True
    return False
 
#
# Funkce s podminkou a se smyckou.
#
def fibonacciIter(n):
    if n <= 1:
        return n
 
    result = 0
    n1 = 0
    n2 = 1
 
    for i in xrange(n-1, 0, -1):
        result = n1 + n2
        n1 = n2
        n2 = result
 
    return result
 
#
# Funkce s rekurzi.
#
def fibonacciRecursive(n):
    if n <= 1:
        return n
    else:
        return fibonacciRecursive(n-1) + fibonacciRecursive(n-2)
 
#
# Vse je nutne otestovat.
#
def main():
    nop1()
    nop2()
    print(answer())
    print(add(1, 2))
    print(add("Hello ", "world!"))
    print(isNegative(-10))
 
    for n in xrange(0,11):
        print(str(n) + "\t" + str(fibonacciIter(n)) + "\t" + str(fibonacciRecursive(n)))
 
#main()
 
def disassemble():
    from dis import dis
 
    print("\nnop1:")
    dis(nop1)
 
    print("\nnop2:")
    dis(nop2)
 
    print("\nanswer:")
    dis(answer)
 
    print("\nadd:")
    dis(add)
 
    print("\nisNegative:")
    dis(isNegative)
 
    print("\nfibonacciIter:")
    dis(fibonacciIter)
 
    print("\nfibonacciRecursive:")
    dis(fibonacciRecursive)
 
disassemble()

Opět se podívejme, jak bude vypadat bajtkód vygenerovaný překladačem Pythonu, resp. přesněji řečeno CPythonu:

Na překladu funkce nop1() pravděpodobně nenajdeme nic překvapivého:

nop1:
 10           0 LOAD_CONST               0 (None)
              3 RETURN_VALUE

Novější Python (3.13 a 3.14) ovšem provede překlad odlišně – bajtkód totiž obecně nemusí být přenositelný mezi různými verzemi Pythonu:

nop1:
  9           RESUME                   0
 10           RETURN_CONST             0 (None)
Poznámka: první instrukce RESUME se používá pro interní trasování atd., jinak neprovádí žádnou další činnost.

Stejným způsobem je přeložena i funkce nop2(), což je pochopitelné:

nop2:
 16           0 LOAD_CONST               0 (None)
              3 RETURN_VALUE

V bajtkódu funkce answer() se na zásobník nejdříve uloží číselná konstanta 42, která je následně instrukcí RETURN_VALUE vrácena volající funkci:

answer:
 22           0 LOAD_CONST               1 (42)
              3 RETURN_VALUE

Bajtkód pro novější Python:

answer:
 21           RESUME                   0
 22           RETURN_CONST             1 (42)

Ve funkci add() se používá instrukce BINARY_ADD, která však ve skutečnosti může pracovat nejenom s čísly, ale i s řetězci, n-ticemi apod., což je velký rozdíl oproti bajtkódu JVM, což jsme ostatně mohli vidět v předchozích kapitolách:

add:
 28           0 LOAD_FAST                0 (x)
              3 LOAD_FAST                1 (y)
              6 BINARY_ADD
              7 RETURN_VALUE

Novější verze bajtkódu obsahuje specifičtější instrukce:

add:
 27           RESUME                   0
 28           LOAD_FAST_LOAD_FAST      1 (x, y)
              BINARY_OP                0 (+)
              RETURN_VALUE

V bajtkódu funkce isNegative() je zajímavá především kombinace instrukcí COMPARE_OP (s operandem <) a JUMP_IF_FALSE. Instrukce bajtkódu Pythonu evidentně nejsou pojmenovány s ohledem na jejich ruční zápis :-):

isNegative:
 34           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (0)
              6 COMPARE_OP               0 (<)
              9 JUMP_IF_FALSE            5 (to 17)
             12 POP_TOP
 
 35          13 LOAD_GLOBAL              0 (True)
             16 RETURN_VALUE
        >>   17 POP_TOP

 36          18 LOAD_GLOBAL              1 (False)
             21 RETURN_VALUE

I v bajtkódu funkce fibonacciIter() nalezneme dvojici instrukcí COMPARE_OPJUMP_IF_FALSE. Kromě toho si povšimněte instrukce FOR_ITER, která pro zadaný iterátor uložený na zásobníku vytvoří základ pro programovou smyčku for:

fibonacciIter:
 42           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (1)
              6 COMPARE_OP               1 (<=)
              9 JUMP_IF_FALSE            5 (to 17)
             12 POP_TOP
 
 43          13 LOAD_FAST                0 (n)
             16 RETURN_VALUE
        >>   17 POP_TOP
 
 45          18 LOAD_CONST               2 (0)
             21 STORE_FAST               1 (result)
 
 46          24 LOAD_CONST               2 (0)
             27 STORE_FAST               2 (n1)
 
 47          30 LOAD_CONST               1 (1)
             33 STORE_FAST               3 (n2)
 
 49          36 SETUP_LOOP              52 (to 91)
             39 LOAD_GLOBAL              0 (xrange)
             42 LOAD_FAST                0 (n)
             45 LOAD_CONST               1 (1)
             48 BINARY_SUBTRACT
             49 LOAD_CONST               2 (0)
             52 LOAD_CONST               3 (-1)
             55 CALL_FUNCTION            3
             58 GET_ITER
        >>   59 FOR_ITER                28 (to 90)
             62 STORE_FAST               4 (i)
 
 50          65 LOAD_FAST                2 (n1)
             68 LOAD_FAST                3 (n2)
             71 BINARY_ADD
             72 STORE_FAST               1 (result)
 
 51          75 LOAD_FAST                3 (n2)
             78 STORE_FAST               2 (n1)
 
 52          81 LOAD_FAST                1 (result)
             84 STORE_FAST               3 (n2)
             87 JUMP_ABSOLUTE           59
        >>   90 POP_BLOCK
 
 54     >>   91 LOAD_FAST                1 (result)
             94 RETURN_VALUE

Bajtkód vygenerovaný novější verzí Pythonu (3.13 a 3.14):

fibonacciIter:
 41           RESUME                   0
 
 42           LOAD_FAST                0 (n)
              LOAD_CONST               1 (1)
              COMPARE_OP              58 (bool(<=))
              POP_JUMP_IF_FALSE        2 (to L1)
 
 43           LOAD_FAST                0 (n)
              RETURN_VALUE
 
 45   L1:     LOAD_CONST               2 (0)
              STORE_FAST               1 (result)
 
 46           LOAD_CONST               2 (0)
              STORE_FAST               2 (n1)
 
 47           LOAD_CONST               1 (1)
              STORE_FAST               3 (n2)
 
 49           LOAD_GLOBAL              1 (xrange + NULL)
              LOAD_FAST                0 (n)
              LOAD_CONST               1 (1)
              BINARY_OP               10 (-)
              LOAD_CONST               2 (0)
              LOAD_CONST               3 (-1)
              CALL                     3
              GET_ITER
      L2:     FOR_ITER                11 (to L3)
              STORE_FAST               4 (i)
 
 50           LOAD_FAST_LOAD_FAST     35 (n1, n2)
              BINARY_OP                0 (+)
              STORE_FAST               1 (result)
 
 51           LOAD_FAST                3 (n2)
              STORE_FAST               2 (n1)
 
 52           LOAD_FAST                1 (result)
              STORE_FAST               3 (n2)
              JUMP_BACKWARD           13 (to L2)
 
 49   L3:     END_FOR
              POP_TOP
 
 54           LOAD_FAST                1 (result)
              RETURN_VALUE

V bajtkódu funkce fibonacciRecursive() si povšimněte zejména instrukce pojmenované LOAD_GLOBAL, kterou je možné využít pro uložení hodnoty globálního symbolu na zásobník:

fibonacciRecursive:
 60           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (1)
              6 COMPARE_OP               1 (<=)
              9 JUMP_IF_FALSE            5 (to 17)
             12 POP_TOP
 
 61          13 LOAD_FAST                0 (n)
             16 RETURN_VALUE
        >>   17 POP_TOP

 63          18 LOAD_GLOBAL              0 (fibonacciRecursive)
             21 LOAD_FAST                0 (n)
             24 LOAD_CONST               1 (1)
             27 BINARY_SUBTRACT
             28 CALL_FUNCTION            1
             31 LOAD_GLOBAL              0 (fibonacciRecursive)
             34 LOAD_FAST                0 (n)
             37 LOAD_CONST               2 (2)
             40 BINARY_SUBTRACT
             41 CALL_FUNCTION            1
             44 BINARY_ADD
             45 RETURN_VALUE
             46 LOAD_CONST               0 (None)
             49 RETURN_VALUE

15. Mezikódy používané v GCC

V ekosystému překladačů GCC se můžeme setkat s několika typy mezijazyků resp. mezikódu. Celý proces překladu je totiž rozdělen do mnoha fází a v každé fázi je možné si výsledek příslušné transformace prohlédnout, i když je nutné na tomto místě poznamenat, že (na rozdíl od dále zmíněného LLVM IR) jsou mezijazyky GCC považovány za interní záležitost překladače a nemusí tak být příliš stabilní (je tomu tak naschvál – aby se zamezilo využití/zneužití části GCC v komerčních překladačích). V každém případě se v moderním GCC setkáme s mezijazyky GIMPLE (GNU IMPLEmentation) a RTL (Register Transfer Language). Těmto mezijazykům bude věnován celý samostatný článek, ovšem v navazující kapitole si alespoň ve stručnosti ukážeme, jak mohou výsledky jednotlivých fází překladu vypadat v případě, že si vyžádáme jejich výpis (jedná se o operaci typu dump, ostatně přesně tak se jmenují i přepínače GCC, které výpis zajistí).

16. Překlad výpočtu do mezikódu GIMPLE a RTL

Překládat budeme dvojici funkcí, které provádí součet, součin a rozdíl celočíselných hodnot. První z těchto funkcí zpracovává hodnoty bez znaménka:

unsigned int addX(unsigned int a, unsigned int b, unsigned int c, unsigned int d) {
    return a+b*c-d;
}

Druhá funkce zpracovává hodnoty se znaménkem (a přitom neřešení přetečení, což je nedefinovaná operace):

signed int addX(signed int a, signed int b, signed int c, signed int d) {
    return a+b*c-d;
}

Výsledek transformace přes AST do mezijazyka GIMPLE dopadne takto:

unsigned int addX (unsigned int a, unsigned int b, unsigned int c, unsigned int d)
{
  unsigned int D.2838;
 
  _1 = b * c;
  _2 = a + _1;
  D.2838 = _2 - d;
  return D.2838;
}

a:

int addX (int a, int b, int c, int d)
{
  int D.2838;
 
  _1 = b * c;
  _2 = a + _1;
  D.2838 = _2 - d;
  return D.2838;
}
Poznámka: jedná se o typovaný kód, který má ovšem jednodušší výrazy (jsou maximálně tříadresové).

Překlad v pozdější fázi už vypadá odlišně:

unsigned int addX (unsigned int a, unsigned int b, unsigned int c, unsigned int d)
{
  unsigned int D.2838;
  unsigned int _1;
  unsigned int _2;
  unsigned int _7;
 
  <bb 2> :
  _1 = b_3(D) * c_4(D);
  _2 = a_5(D) + _1;
  _7 = _2 - d_6(D);
 
  <bb 3> :
<L0>:
  return _7;
}

Dtto pro druhou funkci:

int addX (int a, int b, int c, int d)
{
  int D.2838;
  int _1;
  int _2;
  int _7;
 
  <bb 2> :
  _1 = b_3(D) * c_4(D);
  _2 = a_5(D) + _1;
  _7 = _2 - d_6(D);
 
  <bb 3> :
<L0>:
  return _7;
}

Výstup do RTL používá LISPovskou notaci (RMS se nezapře):

(note 1 0 7 NOTE_INSN_DELETED)
(note 7 1 26 2 [bb 2] NOTE_INSN_BASIC_BLOCK)
(insn/f 26 7 27 2 (set (mem:DI (pre_dec:DI (reg/f:DI 7 sp)) [0  S8 A8])
        (reg/f:DI 6 bp)) "add_unsigned.c":1:82 69 {*pushdi2_rex64}
     (nil))
(insn/f 27 26 28 2 (set (reg/f:DI 6 bp)
        (reg/f:DI 7 sp)) "add_unsigned.c":1:82 95 {*movdi_internal}
     (nil))
(insn 28 27 29 2 (set (mem/v:BLK (scratch:DI) [0  A8])
        (unspec:BLK [
                (mem/v:BLK (scratch:DI) [0  A8])
            ] UNSPEC_MEMORY_BLOCKAGE)) "add_unsigned.c":1:82 1487 {*memory_blockage}
     (nil))
(note 29 28 2 2 NOTE_INSN_PROLOGUE_END)
(insn 2 29 3 2 (set (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                (const_int -4 [0xfffffffffffffffc])) [1 a+0 S4 A32])
        (reg:SI 5 di [ a ])) "add_unsigned.c":1:82 96 {*movsi_internal}
     (nil))
(insn 3 2 4 2 (set (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                (const_int -8 [0xfffffffffffffff8])) [1 b+0 S4 A32])
        (reg:SI 4 si [ b ])) "add_unsigned.c":1:82 96 {*movsi_internal}
     (nil))
(insn 4 3 5 2 (set (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                (const_int -12 [0xfffffffffffffff4])) [1 c+0 S4 A32])
        (reg:SI 1 dx [ c ])) "add_unsigned.c":1:82 96 {*movsi_internal}
     (nil))
(insn 5 4 6 2 (set (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                (const_int -16 [0xfffffffffffffff0])) [1 d+0 S4 A32])
        (reg:SI 2 cx [ d ])) "add_unsigned.c":1:82 96 {*movsi_internal}
     (nil))
(note 6 5 9 2 NOTE_INSN_FUNCTION_BEG)
(insn 9 6 10 2 (set (reg:SI 0 ax [102])
        (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                (const_int -8 [0xfffffffffffffff8])) [1 b+0 S4 A32])) "add_unsigned.c":2:15 96 {*movsi_internal}
     (nil))
(insn 10 9 25 2 (parallel [
            (set (reg:SI 0 ax [102])
                (mult:SI (reg:SI 0 ax [102])
                    (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                            (const_int -12 [0xfffffffffffffff4])) [1 c+0 S4 A32])))
            (clobber (reg:CC 17 flags))
        ]) "add_unsigned.c":2:15 593 {*mulsi3_1}
     (nil))
(insn 25 10 11 2 (set (reg:SI 1 dx [orig:98 _1 ] [98])
        (reg:SI 0 ax [102])) "add_unsigned.c":2:15 96 {*movsi_internal}
     (nil))
(insn 11 25 12 2 (set (reg:SI 0 ax [103])
        (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                (const_int -4 [0xfffffffffffffffc])) [1 a+0 S4 A32])) "add_unsigned.c":2:13 96 {*movsi_internal}
     (nil))
(insn 12 11 13 2 (parallel [
            (set (reg:SI 0 ax [orig:99 _2 ] [99])
                (plus:SI (reg:SI 0 ax [103])
                    (reg:SI 1 dx [orig:98 _1 ] [98])))
            (clobber (reg:CC 17 flags))
        ]) "add_unsigned.c":2:13 283 {*addsi_1}
     (expr_list:REG_EQUAL (plus:SI (reg:SI 1 dx [orig:98 _1 ] [98])
            (mem/c:SI (plus:DI (reg/f:DI 19 frame)
                    (const_int -4 [0xfffffffffffffffc])) [1 a+0 S4 A32]))
        (nil)))
(insn 13 12 21 2 (parallel [
            (set (reg:SI 0 ax [orig:100 _7 ] [100])
                (minus:SI (reg:SI 0 ax [orig:99 _2 ] [99])
                    (mem/c:SI (plus:DI (reg/f:DI 6 bp)
                            (const_int -16 [0xfffffffffffffff0])) [1 d+0 S4 A32])))
            (clobber (reg:CC 17 flags))
        ]) "add_unsigned.c":2:17 389 {*subsi_1}
     (nil))
(insn 21 13 30 2 (use (reg/i:SI 0 ax)) "add_unsigned.c":3:1 -1
     (nil))
(note 30 21 31 2 NOTE_INSN_EPILOGUE_BEG)
(insn 31 30 32 2 (set (mem/v:BLK (scratch:DI) [0  A8])
        (unspec:BLK [
                (mem/v:BLK (scratch:DI) [0  A8])
            ] UNSPEC_MEMORY_BLOCKAGE)) "add_unsigned.c":3:1 1487 {*memory_blockage}
     (nil))
(insn/f 32 31 33 2 (set (reg/f:DI 6 bp)
        (mem:DI (post_inc:DI (reg/f:DI 7 sp)) [0  S8 A8])) "add_unsigned.c":3:1 77 {*popdi1}
     (expr_list:REG_CFA_DEF_CFA (plus:DI (reg/f:DI 7 sp)
            (const_int 8 [0x8]))
        (nil)))
(jump_insn 33 32 34 2 (simple_return) "add_unsigned.c":3:1 1489 {simple_return_internal}
     (nil)
 -> simple_return)
(barrier 34 33 23)
(note 23 34 0 NOTE_INSN_DELETED)

Ještě si pokusme přeložit funkci pro výpočet Fibonacciho posloupnosti.

Původní zdrojový kód:

long fibonacciIter(int n) {
    if (n <= 1) {
        return n;
    }
    long result = 0;
    long n1 = 0;
    long n2 = 1;
    for(n--; n > 0; n--) {
        result = n1 + n2;
        n1 = n2;
        n2 = result;
    }
    return result;
}

Překlad do GIMPLE:

long int fibonacciIter(int n)
{
  long int D.2844;
  long int result;
  long int n1;
  long int n2;
 
  if (n <= 1) goto <D.2842>; else goto <D.2843>;
  <D.2842>:
  D.2844 = (long int) n;
  // predicted unlikely by early return (on trees) predictor.
  return D.2844;
  <D.2843>:
  result = 0;
  n1 = 0;
  n2 = 1;
  n = n + -1;
  goto <D.2840>;
  <D.2839>:
  result = n1 + n2;
  n1 = n2;
  n2 = result;
  n = n + -1;
  <D.2840>:
  if (n > 0) goto <D.2839>; else goto <D.2837>;
  <D.2837>:
  D.2844 = result;
  return D.2844;
}

Po provedení optimalizací:

;; Function fibonacciIter (fibonacciIter, funcdef_no=0, decl_uid=2832, cgraph_uid=1, symbol_order=0)
 
long int fibonacciIter(int n)
{
  long int n2;
  long int n1;
  long int result;
  long int D.2844;
  long int _5;
  long int _11;
  long int _16;
 
  <bb 2> :
  if (n_6(D) <= 1)
    goto <bb 3>; [INV]
  else
    goto <bb 4>; [INV]
 
  <bb 3> :
  _16 = (long int) n_6(D);
  // predicted unlikely by early return (on trees) predictor.
  goto <bb 8>; [INV]
 
  <bb 4> :
  result_7 = 0;
  n1_8 = 0;
  n2_9 = 1;
  n_10 = n_6(D) + -1;
  goto <bb 6>; [INV]
 
  <bb 5> :
  result_12 = n1_3 + n2_4;
  n1_13 = n2_4;
  n2_14 = result_12;
  n_15 = n_1 + -1;
 
  <bb 6> :
  # n_1 = PHI <n_10(4), n_15(5)>
  # result_2 = PHI <result_7(4), result_12(5)>
  # n1_3 = PHI <n1_8(4), n1_13(5)>
  # n2_4 = PHI <n2_9(4), n2_14(5)>
  if (n_1 > 0)
    goto <bb 5>; [INV]
  else
    goto <bb 7>; [INV]
 
  <bb 7> :
  _11 = result_2;
 
  <bb 8> :
  # _5 = PHI <_16(3), _11(7)>
<L5>:
  return _5;
}

17. LLVM IR

V závěru dnešního článku se ještě musíme zmínit o pravděpodobně největší konkurenci ke GCC. Je jím ekosystém postavený okolo projektu LLVM, kterému se budeme podrobněji věnovat v navazujícím článku. V LLVM je překlad rozdělen do většího množství fází. První fáze překladu jsou provedeny frontend překladači, mezi něž patří například Clang. A naopak závěrečné fáze překladu (cílené pro konkrétní operační systém a architekturu mikroprocesorů) jsou prováděny v backend překladači. Mezi těmito fázemi je překládaný program reprezentován v mezikódu/mezijazyku nazývaném LLVM IR (IR=Intermediate Representation). Ten se do určité míry podobá assemblerům RISCových mikroprocesorů, ovšem veškeré argumenty předávané do funkcí, lokální proměnné atd. jsou silně typované. Navíc je LLVM IR navržen takovým způsobem, aby umožňoval překlad pro všechny moderní typy mikroprocesorů a mikrořadičů.

LLVM

Obrázek 5: Fáze překladu v rodině Clang+LLVM s rozdělením na frontend a backend fáze. Uprostřed se nachází reprezentace založená na LLVM IR. 

Autor: tisnik, podle licence: Rights Managed

18. Překlad výpočtu do LLVM IR

Abychom si alespoň ve stručnosti přiblížili, jak LLVM IR vypadá, necháme si do něj přeložit funkci pro součet, součin a rozdíl celočíselných hodnot bez znaménka:

unsigned int addX(unsigned int a, unsigned int b, unsigned int c, unsigned int d) {
    return a+b*c-d;
}

Příslušný soubor s výsledkem překladu do LLVM IR (pro architekturu x86–64 a se zapnutými optimalizacemi) bude mít tvar:

; ModuleID = 'add_unsigned.c'
source_filename = "add_unsigned.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-redhat-linux-gnu"
 
; Function Attrs: mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none) uwtable
define dso_local noundef i32 @addX(i32 noundef %0, i32 noundef %1, i32 noundef %2, i32 noundef %3) local_unnamed_addr #0 {
  %5 = mul i32 %2, %1
  %6 = add i32 %5, %0
  %7 = sub i32 %6, %3
  ret i32 %7
}
 
attributes #0 = { mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none) uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
 
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
 
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"uwtable", i32 2}
!2 = !{!"clang version 20.1.8 (Fedora 20.1.8-4.fc42)"}

Kromě mnoha dalších (meta)informací zde nalezneme i funkci přeloženou do mezikódu. Povšimněte si, že se jedná o kód, ve kterém zůstaly informace o datových typech parametrů i návratové hodnoty funkce:

define dso_local noundef i32 @addX(i32 noundef %0, i32 noundef %1, i32 noundef %2, i32 noundef %3) local_unnamed_addr #0 {
  %5 = mul i32 %2, %1
  %6 = add i32 %5, %0
  %7 = sub i32 %6, %3
  ret i32 %7
}

Zajímavěji dopadne překlad podobné funkce, ovšem zpracovávající celočíselné hodnoty se znaménkem:

signed int addX(signed int a, signed int b, signed int c, signed int d) {
    return a+b*c-d;
}

Výsledek překladu do LLVM IR:

; ModuleID = 'add_signed.c'
source_filename = "add_signed.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-redhat-linux-gnu"
 
; Function Attrs: mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none) uwtable
define dso_local i32 @addX(i32 noundef %0, i32 noundef %1, i32 noundef %2, i32 noundef %3) local_unnamed_addr #0 {
  %5 = mul nsw i32 %2, %1
  %6 = add nsw i32 %5, %0
  %7 = sub i32 %6, %3
  ret i32 %7
}
 
attributes #0 = { mustprogress nofree norecurse nosync nounwind optsize willreturn memory(none) uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
 
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
 
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"uwtable", i32 2}
!2 = !{!"clang version 20.1.8 (Fedora 20.1.8-4.fc42)"}

Mohlo by se zdát, že výsledné pseudoinstrukce jsou prakticky totožné, ale povšimněte si slov nsw. V tomto konkrétním případě to znamená, že pokud dojde k přetečení výsledku operace, je výsledek obecně nedefinovaný (undefined behaviour) a tudíž má backend překladač velkou volnost při optimalizacích a generování výsledného strojového kódu:

Školení Kubernetes

define dso_local i32 @add(i32 noundef %0, i32 noundef %1, i32 noundef %2, i32 noundef %3) local_unnamed_addr #0 {
  %5 = mul nsw i32 %2, %1
  %6 = add nsw i32 %5, %0
  %7 = sub i32 %6, %3
  ret i32 %7
}
Poznámka: jedná se jen o malou ukázku toho, že LLVM IR není ani zdaleka pouze „primitivním“ assemblerem.

Překlad funkce pro výpočet Fibonacciho posloupnosti do LLVM IR:

; ModuleID = 'fibonacci.c'
source_filename = "fibonacci.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-redhat-linux-gnu"
 
; Function Attrs: nofree norecurse nosync nounwind optsize memory(none) uwtable
define dso_local i64 @fibonacciIter(i32 noundef %0) local_unnamed_addr #0 {
  %2 = icmp slt i32 %0, 2
  br i1 %2, label %3, label %5
 
3:                                                ; preds = %1
  %4 = sext i32 %0 to i64
  br label %12
 
5:                                                ; preds = %1, %5
  %6 = phi i64 [ %10, %5 ], [ 1, %1 ]
  %7 = phi i64 [ %6, %5 ], [ 0, %1 ]
  %8 = phi i32 [ %9, %5 ], [ %0, %1 ]
  %9 = add nsw i32 %8, -1
  %10 = add nsw i64 %6, %7
  %11 = icmp samesign ugt i32 %8, 2
  br i1 %11, label %5, label %12, !llvm.loop !3
 
12:                                               ; preds = %5, %3
  %13 = phi i64 [ %4, %3 ], [ %10, %5 ]
  ret i64 %13
}
 
attributes #0 = { nofree norecurse nosync nounwind optsize memory(none) uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
 
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
 
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"uwtable", i32 2}
!2 = !{!"clang version 20.1.8 (Fedora 20.1.8-4.fc42)"}
!3 = distinct !{!3, !4}
!4 = !{!"llvm.loop.mustprogress"}
Poznámka: zde se již používají skoky, které si podrobněji popíšeme příště.

19. Obsah navazujícího článku

V dnešním článku byly popsány různé technologie a postupy, které jsou využívané různými typy překladačů a interpretrů. Jelikož se jednotlivé technologie od sebe mnohdy značně odlišují, nebylo je možné popsat podrobněji. Z tohoto důvodu se v navazujícím článku zaměříme pouze na jediný překladač, a to konkrétně na Clang a LLVM. Popíšeme si zejména mezijazyk LLVM IR, který je navržen takovým způsobem, aby na jedné straně dokázal s rozumnou úrovní abstrakce popsat zdrojové kódy zapisované v různých programovacích jazycích a na straně druhé aby ho bylo možné použít pro vygenerování spustitelného strojového kódu pro různé platformy, a to včetně těch platforem, které podporují pokročilé SIMD operace a popř. i souběh různých částí kódu běžících v odlišných vláknech. Jak v článku uvidíme, je LLVM IR sice zpočátku poměrně nečitelný, ale po krátkém zaučení umožňuje sledovat fáze překladu, použité optimalizační metody atd.

20. Odkazy na Internetu

  1. Is intermediate representation (such as bytecodes or .net IL) still an advantage?
    https://stackoverflow.com/qu­estions/35061333/is-intermediate-representation-such-as-bytecodes-or-net-il-still-an-advantage
  2. Intermediate Representation vs Byte Code
    https://cs.stackexchange.com/qu­estions/163398/intermedia­te-representation-vs-byte-code
  3. Getting the intermediate representation in gcc
    https://forum.osdev.org/vi­ewtopic.php?t=22845&sid=11f6e77­d24bda7fcc2c9ef6a5be4e6b2
  4. Intermediate Representations
    https://www.cs.cornell.edu/cou­rses/cs4120/2023sp/notes/ir/
  5. Why do we use intermediate representations / languages?
    https://mortoray.com/why-we-use-intermediate-representations/
  6. Unwrapping intermediate representations
    https://mortoray.com/unwrapping-intermediate-representations/
  7. Understanding Python Code Flow From Source to Execution
    https://medium.com/@azan96593/un­derstanding-python-code-flow-from-source-to-execution-ebeea870ef83
  8. Why most compilers use AST, instead generate IR directly?
    https://stackoverflow.com/qu­estions/60870622/why-most-compilers-use-ast-instead-generate-ir-directly#60902159
  9. A Gentle Introduction to LLVM IR
    https://mcyoung.xyz/2023/08/01/llvm-ir/
  10. Why does the compiler need the intermediate representations for link time optimization?
    https://stackoverflow.com/qu­estions/75586563/why-does-the-compiler-need-the-intermediate-representations-for-link-time-optimi
  11. pyrefact na PyPi
    https://pypi.org/project/pyrefact/
  12. Repositář projektu pyrefact
    https://github.com/OlleLin­dgren/pyrefact
  13. pyrefact jako plugin do VSCode
    https://marketplace.visual­studio.com/items?itemName=o­lleln.pyrefact
  14. pyrefact-vscode-extension (repositář)
    https://github.com/OlleLin­dgren/pyrefact-vscode-extension
  15. Best Python Refactoring Tools for 2023
    https://www.developer.com/lan­guages/python/best-python-refactoring-tools/
  16. Python Refactoring: Techniques, Tools, and Best Practices
    https://www.codesee.io/learning-center/python-refactoring
  17. Lexikální a syntaktická analýza zdrojových kódů programovacího jazyka Python
    https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-programovaciho-jazyka-python/
  18. Lexikální a syntaktická analýza zdrojových kódů programovacího jazyka Python (2.část)
    https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-programovaciho-jazyka-python-2-cast/
  19. Lexikální a syntaktická analýza zdrojových kódů programovacího jazyka Python (3.část)
    https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-programovaciho-jazyka-python-3-cast/
  20. Lexikální a syntaktická analýza zdrojových kódů jazyka Python (4.část)
    https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-jazyka-python-4-cast/
  21. Knihovna LibCST umožňující snadnou modifikaci zdrojových kódů Pythonu
    https://www.root.cz/clanky/knihovna-libcst-umoznujici-snadnou-modifikaci-zdrojovych-kodu-pythonu/
  22. LibCST – dokumentace
    https://libcst.readthedoc­s.io/en/latest/index.html
  23. libCST na PyPi
    https://pypi.org/project/libcst/
  24. libCST na GitHubu
    https://github.com/Instagram/LibCST
  25. Inside The Python Virtual Machine
    https://leanpub.com/insidet­hepythonvirtualmachine
  26. module-py_compile
    https://docs.python.org/3­.8/library/py_compile.html
  27. Given a python .pyc file, is there a tool that let me view the bytecode?
    https://stackoverflow.com/qu­estions/11141387/given-a-python-pyc-file-is-there-a-tool-that-let-me-view-the-bytecode
  28. The structure of .pyc files
    https://nedbatchelder.com/blog/200804/the_str­ucture_of_pyc_files.html
  29. Python Bytecode: Fun With Dis
    http://akaptur.github.io/blog/2013/08/14/pyt­hon-bytecode-fun-with-dis/
  30. Python's Innards: Hello, ceval.c!
    http://tech.blog.aknin.na­me/category/my-projects/pythons-innards/
  31. Golang Compilation and Execution
    https://golangtutorial.com/golang-compilation-and-execution/
  32. Mezijazyk (Wikipedie)
    https://cs.wikipedia.org/wi­ki/Mezijazyk
  33. The LLVM Compiler Infrastructure
    https://www.llvm.org/
  34. GCC internals
    https://gcc.gnu.org/online­docs/gccint/index.html
  35. GCC Developer Options
    https://gcc.gnu.org/online­docs/gcc/Developer-Options.html
  36. What is Gimple?
    https://mschiralli1.wordpres­s.com/2024/12/01/what-is-gimple/
  37. The Conceptual Structure of GCC
    https://www.cse.iitb.ac.in/grc/in­tdocs/gcc-conceptual-structure.html
  38. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  39. ASM Home page
    http://asm.ow2.org/
  40. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  41. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  42. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  43. BCEL Home page
    http://commons.apache.org/bcel/
  44. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  45. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  46. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  47. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  48. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  49. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  50. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  51. Javassist
    http://www.jboss.org/javassist/
  52. Byteman
    http://www.jboss.org/byteman
  53. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  54. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  55. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  56. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  57. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  58. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  59. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  60. Abstract syntax tree
    https://en.wikipedia.org/wi­ki/Abstract_syntax_tree
  61. Lexical analysis
    https://en.wikipedia.org/wi­ki/Lexical_analysis
  62. Parser
    https://en.wikipedia.org/wi­ki/Parsing#Parser
  63. Parse tree
    https://en.wikipedia.org/wi­ki/Parse_tree
  64. Derivační strom
    https://cs.wikipedia.org/wi­ki/Deriva%C4%8Dn%C3%AD_strom
  65. Python doc: ast — Abstract Syntax Trees
    https://docs.python.org/3/li­brary/ast.html
  66. Python doc: tokenize — Tokenizer for Python source
    https://docs.python.org/3/li­brary/tokenize.html
  67. SymbolTable
    https://docs.python.org/3­.8/library/symtable.html
  68. 5 Amazing Python AST Module Examples
    https://www.pythonpool.com/python-ast/
  69. Intro to Python ast Module
    https://medium.com/@wshanshan/intro-to-python-ast-module-bbd22cd505f7
  70. Golang AST Package
    https://golangdocs.com/golang-ast-package
  71. AP8, IN8 Regulární jazyky
    http://statnice.dqd.cz/home:inf:ap8
  72. AP9, IN9 Konečné automaty
    http://statnice.dqd.cz/home:inf:ap9
  73. AP10, IN10 Bezkontextové jazyky
    http://statnice.dqd.cz/home:inf:ap10
  74. AP11, IN11 Zásobníkové automaty, Syntaktická analýza
    http://statnice.dqd.cz/home:inf:ap11
  75. Introduction to YACC
    https://www.geeksforgeeks­.org/introduction-to-yacc/
  76. Introduction of Lexical Analysis
    https://www.geeksforgeeks­.org/introduction-of-lexical-analysis/?ref=lbp
  77. Write your own filter
    http://pygments.org/docs/fil­terdevelopment/
  78. Write your own lexer
    http://pygments.org/docs/le­xerdevelopment/
  79. Write your own formatter
    http://pygments.org/docs/for­matterdevelopment/
  80. Compiler Construction/Lexical analysis
    https://en.wikibooks.org/wi­ki/Compiler_Construction/Le­xical_analysis
  81. Compiler Design – Lexical Analysis
    https://www.tutorialspoin­t.com/compiler_design/com­piler_design_lexical_analy­sis.htm
  82. Lexical Analysis – An Intro
    https://www.scribd.com/do­cument/383765692/Lexical-Analysis
  83. Python AST Visualizer
    https://github.com/pombredanne/python-ast-visualizer
  84. What is an Abstract Syntax Tree
    https://blog.bitsrc.io/what-is-an-abstract-syntax-tree-7502b71bde27
  85. Why is AST so important
    https://medium.com/@obernar­dovieira/why-is-ast-so-important-b1e7d6c29260
  86. Emily Morehouse-Valcarcel – The AST and Me – PyCon 2018
    https://www.youtube.com/wat­ch?v=XhWvz4dK4ng
  87. Python AST Parsing and Custom Linting
    https://www.youtube.com/wat­ch?v=OjPT15y2EpE
  88. Chase Stevens – Exploring the Python AST Ecosystem
    https://www.youtube.com/wat­ch?v=Yq3wTWkoaYY
  89. Full Grammar specification
    https://docs.python.org/3/re­ference/grammar.html
  90. Playing with GCC’s GIMPLE: How to Generate, Save, and Modify Intermediate Code (Tutorial + Examples)
    https://www.tutorialpedia­.org/blog/playing-with-gcc-s-intermediate-gimple-format/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.