„Skriptování“

Tento článek je určen programátorům, kteří by si chtěli zkusit naprogramovat vlastní skriptovací jazyk (například do hry nebo jako chytrého FTP klienta), ale nevědí, jak začít. Pokusím se s vámi podělit o své – i když ne příliš velké – zkušenosti v této oblasti a poradit, v čem budu moct.

V krátkosti – co je skript, co je to skriptovací jazyk a čím se liší od jazyku programovacího: skript (v intencích programování) je většinou jednoduchý a krátký program (tedy popis jednotlivých kroků spolu se způsobem, jak jsou za sebou řazeny) ve své zdrojové podobě, který zpracovává tzv. interpreter (což je proces, který skript čte a postupně vykonává úkony v něm napsané). Skriptovací jazyk je pak soupis všech příkazů a pravidel, které je ve skriptu možno použít. Skript se vždy zpracovává ve zdrojové podobě – zdrojový program se od skriptu liší další fází, ve které se zdrojový program překládá do spustitelného kódu, srozumitelného pouze pro procesor počítače (který je však vykonáván rychleji).

Podívejme se nejdříve, z čeho se takový skriptovací zdrojový program skládá: obsahuje zcela určitě nějaké příkazy. Příkazy jsou občas tu a tam sdruženy do řídicích struktur. Propracovanější skriptovací jazyky mohou obsahovat i podporu funkcí nebo dokonce objektů, ale až tak daleko nepůjdeme. Posledním stavebním prvek, který v této fázi stojí za řeč, jsou direktivy.

r=1;
if(b) {
 for(int i=1;i<=b;i++){
 r*=i;
 }
}	r:=1;
if b<>0 then begin
 for i:=1 to b do begin
  r:=r*i;
 end;
end;	$r=1;
if($b):
 for($i=1; $i<=b; $i++):
  $r*=$i;
 endfor;
endif;	r=1
If b<>0 Then
 For i=1 To b
  r=r*i
 Next
End If	class x … {
 int b,r;
 public y(){
  if(b){
   for(int i=1; i<=b; i++){
    r*=i;
   }
  }
 }
}	
C	Pascal	PHP	Visual Basic	Java	
tabulka 1: Ukázka nějakého přidebilnělého kódu v různých skriptovacích a programovacích jazycích. Termy řídicích struktur jsou zvýrazněny tučně.
Řídicí struktury
Co myslím těmi řídicími strukturami: to jsou všechny možné konstrukce typu if…endif (nebo, chcete-li if{…}), do…while, repeat (loop)…until, for…next nebo for{…} apod. Mezi řídicí konstrukce může být započítána vlastně i funkce, metoda nebo celá třída (samozřejmě, pouze z pohledu programátora, který je má implementovat ve vlastním skriptovacím jazyku).
A co jsou to direktivy také víme, ale pro úplnost: direktivy je zvláštní druh instrukcí, určených ne pro samotný program, ale jeho zavedení do paměti a vymezení vůbec toho, co se má za program určený k interpretaci, považovat. Direktiv si všímá a zpracovává tzv. preprocesor, který je na zdroják aplikován před překladačem nebo interpreterem. Direktivy se hojně používají v programovacích jazycích, ale své místo mají i ve skriptech. Typicky jsou to například instrukce o zavedení a vložení dalších souborů daného jména do zdrojáku, definování symbolů (jež jsou platné pouze po dobu prvního průchodu [průchod – viz dále] programu) a jejich užití pro tzv. podmíněný překlad.
Pokud jsou v daném jazyku direktivy, musí překlad probíhat v minimálně dvou průchodech (též průběh nebo jetí), kde v prvním průběhu budou zpracovány direktivy, které upraví finální podobu zdrojového programu, a v druhém jetí se zpracuje takto vzniknuvší program.
Teďky, co potřebujeme k nejjednoduššímu funkčnímu interpreteru skriptovacího jazyka: načíst ze zdrojového souboru řádku textu, načíst z této řádky term (term: nejelementárnější stavební kámen programovacího nebo skriptovacího jazyka – termem může být číslo, povinná závorka, klíčové slovo, název proměnné), a výraz z těchto termů. Pro zjednodušení režie pro zpracování programu si stanovíme následující pravidlo: maximálně jeden úkon na řádek. (Úkonem mám na mysli buď příkaz, direktivu, řídicí strukturu nebo její část.)
Vraťme se ale k řídicím strukturám. Předpokládám, že jste je vám jasné, že tyto struktury se do sebe můžeme libovolně nořit, dokud je vynoření z dané struktury podmíněno zrušením všech struktur v této struktuře. To budeme dodržovat i v našem skriptovacím jazyku. Pro uchování informací o tom, do jakých řídicích struktur je právě běh programu zanořen, použijeme zásobník. Protože budeme používat direktivy a protože je pro nás vhodné dovědět se o všech řídicích strukturách v programu včas (ne až když třeba zjistíme, že jsou do sebe chybně zapuštěny), tak budeme provádět i první průchod, ve kterém všechny lapsy řídicích struktur odhalíme a vyhodíme u nich fatální chybovou hlášku (tento první průchod bude zároveň suplovat preprocessing, jako tomu je u normálních programovacích jazyků). Není to zas tak složité. Například během procházení a zpracování tohoto fragmentu (pseudo)kódu, bude zásobník pro řídicí struktury vypadat následovně:

ř.	kód	stav zásobníku (po vykonání řádku)	změny v seznamu řídicích struktur	
1	if …	[if]	+if(1,…)  //přidání do seznamu	
2	 for …	[if] [for]	+for(2,…)	
3	  while …	[if] [for] [while]	+while(3,…)	
4	   if …	[if] [for] [while] [if]	+if(4,…)	
5	    repeat	[if] [for] [while] [if] [repeat]	+repeat(5)	
6	     …	[if] [for] [while] [if] [repeat]		
7	    until …	[if] [for] [while] [if]	repeat(5) <- {7,…} //úprava prvku	
8	    for …	[if] [for] [while] [if] [for]	+for(8)	
9	     …	[if] [for] [while] [if] [for]		
10	    next	[if] [for] [while] [if]	for(8) <- {10}	
11	   else	[if] [for] [while] [if]	if(4,…) <- {11}	
12	    while …	[if] [for] [while] [if] [while]	+while(12,…)	
13	     …	[if] [for] [while] [if] [while]		
14	    end while	[if] [for] [while] [if] [while]	while(12,…) <- {14}	
15	    if …	[if] [for] [while] [if] [if]	+if(15,…)	
16	     repeat	[if] [for] [while] [if] [if] [repeat]	+repeat(16)	
17	      …	[if] [for] [while] [if] [if] [repeat]		
18	     until …	[if] [for] [while] [if] [if]	repeat(16) <- {18,…}	
19	    end if	[if] [for] [while] [if] 	if(15,…) <- {19}	
20	   end if	[if] [for] [while]	if(4,…,11) <- {20}	
21	  end while	[if] [for]	while(3,…) <- {21}	
22	 next	[if]	for(2,…) <- {22}	
23	end if		if(1,…) <- {23}	
Na konci má seznam řídicích struktur tuto podobu:
prvek	struktura	řádek začátku	řádek konce	
[0]	if	1	23	
[1]	for	2	22	
[2]	while	3	21	
[3]	if	4	20	
[4]	repeat	5	7	
[5]	for	8	10	
[6]	while	12	14	
[7]	if	15	19	
[8]	repeat	16	18	
Všimněte si, že v syntakticky správně napsaném skriptu je vždy na řádce, která ukončuje některý řídicí blok, poslední prvek zásobníku typu právě ukončované struktury. Pokud tomu tak není, je to fatální chyba.
Pokud jsme začínali procházet program s prázdným zásobníkem, musíme taktéž s prázdným zásobníkem procházení skončit (má-li být program syntakticky správně). Taktéž pozor na to, že některé konstrukce vyžadují určitou hloubku zásobníku (samostatné globální funkce a třídy 0, metody 1, apod).
Do zásobníku budeme vkládat jednotlivé struktury během průchodu programem a při vynoření z jedné struktury budeme testovat, zda-li neexistují nějaké dosud neukončené struktury v této struktuře. Pokud ano, je to špatně, a doporučil bych vyhodit chybovou hlášku a dál v překladu programu nepokračovat. Při prvním průchodu programem nás nezajímají žádné velké detaily, pouze direktivy (a návěští – pokud je implementujeme) a syntaktická správnost užití řídicích struktur.
Dále jsem uvažoval nad tím, co je ještě vhodné si při prvním průchodu zapamatovat. Pokud například pokaždé při vyskočení z dané struktury (ať už nesplněním vstupní podmínky či záměrně) nechci hledat konec bloku procházením zdrojáku, je nanejvýš vhodné si zaznamenat, především typ dané struktury, dále, kde čili na kterém řádku daná struktura začíná (což danou strukturu jednoznačně identifikuje, máme-li jeden příkaz či strukturní term na řádek), dále eventuelně, kde se nacházejí jeho další části (například, mezi if a endif může být několik elseif), na kterém sloupci začíná výraz podmínky pro setrvání v dané struktuře a podobně.
Při procházení řádků a procházení jednotlivých elementárních termů na každém řádku se mi osvědčilo mít jednu globální proměnnou pro aktuálně zpracovávaný řádek a sloupec, do nějž pokročilo parsování úkonu na daném řádku, a každou oblast ve skriptu identifikovat pomocí řádku (nemůžeme-li mít na jednom řádku více úkonů, jakože to předpokládáme), pak je tato identifikace jednoznačná, a tedy dostatečná.
Po prvním (kontrolním) průchodu jsme tedy vykonali direktivy, nalezli odpověď na to, jestli je program syntakticky správně, a máme základní informace o řídicích strukturách. Ve výše uvedené tabulce, dva poslední sloupečky odpovídají obsahu dvou polí: je třeba si uvědomit, že první z nich je zásobník, který reflektuje okamžitý, současný stav zanoření do řídicích struktur (tedy může mít v každém místě v programu jiný obsah), a druhé je jakási kartotéka řídicích struktur, použitých v aktuálně prováděném skriptu. Zatímco zásobník se nám po průchodu sám vyprázdní, v „kartotéce“ se nám nashromáždily ‚drahé‘ informace, které při samotné interpretaci programu s výhodou využijeme.
Přejděme tedy na druhý průběh programu. To už jedeme „naostro“, zajímají nás tedy všechny detaily skriptu, které pro jeho fungování mají význam. 
Tabulka symbolů
Při probírání interpretace skriptu se nelze nezmínit o tzv. tabulce symbolů. Tabulka symbolů, jak jistě víte, je seznam všech symbolů, definovaných v skriptu. Symbolem se většinou myslí proměnná, ale může to být i návěští, jméno funkce, structu nebo unionu a cojáviještěčeho. (poslední zmíněný zavádí C# :-))
Mno, já jsem si takový pokusný interpreter spáchal v Delphi, kde jsem mohl využít typ variant (který existuje určitě i v Borland C++). Tadyta „vymoženost“ (tedy, typ variant) mi jednak poskytla univerzální typ pro hodnoty v tabulce symbolů, ale především mi posloužila k implementaci zásobníku i seznamu řídicích struktur (protože se z variantu dá zkonstruovat i pole). A tak můžete mít buď dvě jednorozměrná pole pro názvy symbolů a hodnoty symbolů, a nebo pole polí, v jehož prvním prvku bude vždy název symbolu a v druhém jeho hodnota.
Je lepší, pokud ve vašem skriptovacím jazyku zajistíte bližší spolupráci s tabulkou symbolů. Například PHP, též skriptovací jazyk, umožňuje proměnné z tabulky symbolů smazat (undef()) nebo se dotázat na existenci symbolu v tabulce pomocí jeho identifikátoru (isvar()). Implementovat toto je jednoduché, tak nevidím důvod, proč to uživateli nedopřát. Díky existenci typu variant jsem mohl vlastně obejít tu skutečnost, že spolu se symbolem bych do tabulky měl ukládat i jeho typ. Ale jsou případy, kdy je potřeba krom názvu a hodnoty symbolu si zaznamenat ještě něco navíc. Takovým případem jsou konstanty (pokud chceme zajistit, aby se ve skriptu nemohla jejich hodnota později změnit) a identifikátor funkce v momentu jejího zpracování.
Funkce
Funkce v našem skriptu není nic víc než další typ řídicí struktura (která se nenalézá uvnitř jiné struktury, neboť funkce jsou v našem skriptu globální). Pokusím se vysvětlit jeden z možných způsobů, jak funkce v našem skriptu implementovat. Po prvním průchodu a kontrole skriptu již v seznamu řídicích struktur máme uloženy všechny funkce, víme, kde začínají, popřípadě, kolik a jaké mají parametry. Budeme předpokládat, že závorky za každou funkcí jsou povinné, i kdyby mezi nimi nebyly žádné parametry. Tím funkce bez hledání poznáme při vyhodnocování výrazů. Když je při vyhodnocování výrazu nalezen identifikátor funkce, zavolá se v interpreteru metoda, která má za úkol, její hodnotu zjistit. Ta provede to, že nejdříve vloží do tabulky symbolů identifikátor funkce a současně u něj označí, že je to identifikátor funkce (buď  pomocí nějakého speciálního znaku nebo extra proměnné, která se do tabulky bude muset ukládat spolu s názvem a hodnotou symbolu). Tento symbol bude v tabulce symbolů působit jako jakási „zarážka“ – všechno od začátku tabulky (tedy od indexu 0 v tomto poli) až do první zarážky jsou globální proměnné. Všechno od konce tabulky (nejvyššího indexu) až po poslední uvedenou zarážku jsou lokální symboly, s nimiž pracuje funkce. To, že symboly se do tabulky symbolů ukládají v tom pořadí, ve kterém v kódu vyskytují, je vlastnost, kterou využijeme. Tabulka symbolů se totiž vstoupením do funkce začne chovat jako zásobník. Proč? Protože tabulka musí v samotné funkci zajistit definici nových (lokálních) symbolů, ale současně by je zase měla smazat při opuštění funkce. Je celkem logické, že z funkce je možné volat jinou funkci – tabulka symbolů se při takovémto dalším volání opět „nakupí“ o nové lokální symboly (jež předchází „zarážka“ v podobě identifikátoru funkce). Po ukončení naposledy volané funkce tato po sobě „vyčistí“ svou část tabulky symbolů a pokračuje se dále. Toto je příklad z mého skriptovacího jazyka, kdy je běh programu v určitém momentu vnořen do dvou různých funkcí zároveň.

function soucin(a=0,b=0)
 return(a*b)
end_function
function faktorial(n=1)
 var i
 for(i=1;i<=n;i=i+1)
  result=soucin(result,i)
 end_for
 return(result)
end_function
log faktorial(5)

Tabulka symbolů v jednotlivých fázích vyhodnocení tohoto kódu: modře jsou vyznačeny nepravé symboly (identifikátory funkcí), které tvoří zarážky, ohraničující dostupné symboly v tabulce (dostupné symboly jsou od prvního až po první zarážku nevčetně plus konce po poslední zarážku včetně). Pokud je v tabulce alespoň jedna zarážka, symboly nalevo od první zarážky označuji jako globální, symboly napravo od poslední jsou pak lokální symboly. Nedostupné symboly jsou zvýrazněny šedou barvou.
(globální symboly)	

(globální symboly)	faktorial	

(globální symboly)	faktorial	n	i	

(globální symboly)	faktorial	n	i	soucin	

(globální symboly)	faktorial	n	i	soucin	a	b	

(globální symboly)	faktorial	n	i	

(globální symboly)	
Přestože zarážky považuji za nepravé symboly, můžu je přecejen využít. Funkce přece vrací nějaké výsledky, tak proč je nevkládat právě do odpovídajících symbolů, definovaných – i když jen dočasně – v tabulce symbolů?
Vraťme se ale k funkci interpreteru: ta by měla do tabulky symbolů vložit identifikátor funkce, sloužící jako zarážka a současně výsledek (návratová hodnota) funkce, následovaný parametry funkce (pokud nějaké jsou) a poté přenést dočasně běh programu do těla funkce. Dospěje-li běh programu na konec těla funkce nebo je-li zavolán příkaz return, musí se funkce interpreteru postarat o vyklizení dosavadních lokálních symbolů a identifikátoru funkce z tabulky symbolů a vrátit hodnotu funkce, kterou doposud zpracovávala.
Poznámka: Od doby, kdy jsem tento odstavec napsal, uplynulo několik dnů, a já už tento problém ve svém skriptovacím jazyku vyřešil poněkud „slušněji“ – tabulku symbolů jsem rozšířil tak, že nyní na místo hodnoty každého symbolu uloží variantní pole s položkami: jméno, hodnota a příznaky, kde položka příznaky slouží mimo jiné k tomu, aby identifikovala nepravý symbol – název funkce. Toto řešení se mi jeví jako elegantnější a doporučil bych ho spíš než umisťování nějakých zarážek.
GoTo a GoSub
Pokud z nějakého důvodu nechcete funkce a spokojíte se s voláním podprogramů à la Basic, pak vězte, že to má jedno úskalí. Ono totiž nestačí pouze nastavit běh programu na cílový řádek – navíc je potřeba zanalyzovat, v jaké řídicí struktuře cílová řádka leží a podle toho upravit zásobník řídicích struktur. A nebo si tento stav ukládat spolu s návěštím a při (od)skoku tento zásobník nahradit tím uloženým. Mně se však toto řešení nezdá příliš čisté kvůli neefektivnosti a relativní zátěži paměti.
Ale nejdřív bych se zeptal, zda-li je podpora těchto dvou řídicích příkazů opravdu nezbytná. Implementace brejku s volitelným počtem úrovní, které má běh programu opustit, a podpora funkcí, navíc s implicitními argumenty, jsou více než dostatečnými nástroji pro řízení běhu programu; a implementace těchto dalších funkcí by mohla ryzí principy našeho skriptovacího jazyka znesvětit. Když už jsme si dali tu práci s výstavbou tolika struktur s přehledným tokem děje programu, bylo by kontraproduktivní jej porušit těmito dvěma nízkoúrovňovými příkazy. Proto to ještě jednou zvažte.
Soubory a práce s nimi
Leckteré skriptovací a programovací jazyky umožňují, aby proměnná představovala otevřený soubor, z něhož je možno číst nebo do něj zapisovat. Soubory ale nelze brát jako proměnné, nad nimiž by fungovaly stejné aritmetické, logické či jiné operace jako u čísel, řetězců, datumů apod. Máme dvě možnosti: buďto umožnit, aby proměnné mohly představovat i soubory – a od toho okamžiku důsledně rozlišovat typy proměnných ve výrazech a výstup chybových hlášek při pokusu o zapojení „souborové“ proměnné do výrazu – nebo práci se soubory umožnit jinou cestou (například v programech jazyka Basic se otevřené soubory zapisují jako čísla). Můžete se skutečně rozhodnout libovolně. Já se rozhodl hodit všechny otevřené soubory do kolekce a umožnit uživateli přístup k souborům pomocí ní.
Třídy
Fíha - tak daleko jsem se se svým skripťákem zatím nedostal; a dost pochybuji, že v dohledné době dostanu. Ono je přeci jen přinejmenším „lehce nestandardní“ snažit se v objektově orientovaném jazyku naprogramovat… objektově orientovaný jazyk ;-). A ani to není posláním našeho výtvoru, jen o trochu chytřejšího než možnosti příkazové řádky, který chceme využít na jinak otravné, repetitivní úkony.
No, ale abych tady nemluvil o tom, jak udělat něco, co vlastně udělat nechci, naznačím alespoň teoreticky, kudy na to. Především, s použitím tříd nám přibude implementovat strukturu takové třídy (její členy – atributy, vlastnosti, metody, (události)), plus klíčová slova pro vytvoření a zničení instance třídy (obvykle jsou to klíčová slova new a delete). Samotnou třídu bychom museli chápat jako strukturu stejně jako všechny ostatní struktury, které zpracováváme, pouze trochu složitější. Museli bychom též rozšířit tabulku symbolů a přizpůsobit si ji tak, aby věděla, že v jakém okamžiku pracuje v klauzuli (scope) třídy (například v nějaké její metodě) a kdy v globálním skriptu. Úprava skriptu tak, aby uměl krom funkcí parsovat i metody tříd, by byla jen minimální – všechny principy již máme vyřešeny ve spolupráci se zásobníkem řídicích struktur, o kterém se zmiňuji na začátku článku. Horší by to bylo (promiňte, že používám neustále ten podmiňovací způsob – předpokládám, že nikdo z čtenářů teď nesedne a nezačne programovat objektově orientovaný skriptovací jazyk) s klauzulemi tříd (public, private, protected, published,…), správou (nebo spíše emulací správy) VMT – virtuálních tabulek metod, implementací tzv. přátelských atributů mezi dvěma třídami a další opravdu už specialitky, které využijí jen akademičtí programátoři.
Kapitola impelementace tříd tu je jen pro úplnost – to podstatné je, řekl bych, uvedeno v druhém, třetím a čtvrtém odstavci, takže pokud uvažujete nebo si chcete zkusit takový skriptovací jazyk naprogramovat, budiž vám můj článek jako výchozí bod.
Carlos