Hogyan írjunk interpretert 90 perc alatt, ~ 120 sorban [IV. Inter++]

Az interpreterünk, jelenlegi készültségében öt részre tagolódik.

1. Az egyik ilyen rész a főprogram. Innen indul az egész. Meghívása: interpr.exe script.prg >out.txt
2. A másik rész a ProgLoad procedúra. Ez tölti be a scriptünket a Prg tömbbe. A főprogram hívja meg, elsőként.
3. A harmadik a Tokenizer procedúra, ami szintén a főprogram hív meg, a script betöltése után.
A tokenizer a script egy-egy sorát dolgozza föl. A sorok egymás után következnek és elsőbbségüket a pozíciójuk határozza meg.  
4. Ha a Tokenizer végzett egy sorral, akkor visszakerül a vezérlés a főprogramba és onnan hívódik meg az Interpreter.
Ez a programrész hajtja végre az utasításainkat. Lényegében a "Tokenizer" által feltöltött TOKEN nevű tömb nulladik elemétől (TOKEN[0]) függ, hogy a végrehajtás melyik ága fut le (lásd a forrásban az "Interpreter" nevű procedúra CASE utasítását). A CASE-nek annyi ága van, ahány utasítást ismer a mi, semmiből  teremtett script nyelvünk.
Ha mondjuk éppen az INC utasítást tartalmazza a TOKEN[0], akkor a végrehajtás az 'INC' ágba lép és a további történéseket az szabja meg, hogy a TOKEN[0] melletti TOKEN[1] miféle. Annak tartalma ugyanis - ahogy az utasítás-táblázat is jelzi - csak változó (VAR) lehet, az ABC valamelyik betűje.

Ez a betű lesz az eddig nem használt SetVar procedúra egyik paramétere, ez határozza meg, hogy melyik változónk értéke lesz inkrementálva. Hogy mennyivel, azt a TOKEN[2] tartalma határozza meg, Az nyilvánvaló, hogy a TOKEN[2] tartalma nem lehet más, mint egy 65536-nál kisebb, pozitív egész szám.

   Érdemes megfigyelni, --- ez ugyanis nagyon fontos --- , hogy a dologba képzelt hallatlan bonyolultság itt, ezen a ponton vált át egyszerűségre, a CASE utasításban. Hiszen, maga az érkező utasítás determinálja (határozza meg) a következményeket, a később végrehajtandó lépéseket, azok sorrendjét, de még azok számát is. Ha az utasítás mondjuk a SAVE lenne, Akkor annyi történne, hogy a TOKEN[1]-ben tárolt szám (legyen 123), mint fájlnév szerepelne és csak elmentené a képünk tartalmát, valahogy így: Savetofile('123.bmp').

   A lényeg, hogy interpretert ennyire egyszerű megvalósítani, hiszen adott pillanatban nem kell semmi másra koncentrálnunk, mint arra az egyetlen végrehajtás alatt álló sorra.

   Na de mi történik akkor, ha a SAVE utasítás mögötti paraméter nem szám, hanem egy betű, tehát, egy változó? Ez sem megoldhatatlan.

   Erre és hasonló esetekre írni kell egy függvényt, ami kideríti, hogy a TOKEN[1] első karaktere (Ez a TOKEN[1][1], vagy más formában a TOKEN[1,1]), szóval, hogy a token első karaktere betű-e vagy szám.  De hát ilyennel már találkozott is az, aki követte az első sorozatot. Emlékszünk?
Ez volt a Numeric/Alphabet függvénypáros. Ha mondjuk az Alphabet(TOKEN[1,1]) = true, akkor a paraméter egy változó, tehát a GetVar(TOKEN[1,1]) függvény visszatérési értéke lesz a SAVE utasítás paramétere, ellenkező esetben az a szám amit a TOKEN[1] tartalmaz.

Miért van ennek jelentősége? Mert így, ezzel a kis kiegészítéssel már dinamikus lett a nyelvünk! Mire gondolok?  A rugalmasságra, a sokoldalúságra.

Ha például a Z változót használjuk (szám helyett) a rajzolt kép állapotának elmentésére, akkor már a rajzolás egyes fázisait is elmenthetjük, amiből később akár videót, vagy animált .gif-et is készíthetünk. Egy szimpla példa:

LET A,100
LET B,30
;
LOOP 8
GOTO 10,10
RECT A,B
SAVE Z
INC B,10
INC Z,1
ENDLOOP

   Jól követhető, hogy a Z nevű változó nyolc alkalommal inkrementálódik így a kirajzolt képnek is nyolc készültségi fázisa (0.bmp, 1.bmp, 2.bmp, 3.bmp, stb.) kerül mentésre. Tény, ugyanezt változó helyett számokkal is meg lehetne valósítani, de az sokkal hosszabb kódot eredményezne. Tehát, sokkal több munkánkba kerülne ugyanaz az eredmény, pedig nekünk éppen az a célunk, hogy a ráfordított időt csökkentsük.

   A script nyelvünk hatékonysága érzékelhetően nagyot nő ezzel a rugalmassággal. Mármint, hogy egy-egy utasítás paramétere nem csak változó, vagy csak szám lehet, hanem ez is vagy az is. És ráadásul, a bővítés, ami ezt lehetővé teszi, alig került erőfeszítésünkbe.
Gondoljunk bele, mennyivel nőnek a lehetőségeink, ha a legtöbb utasításnak két paraméterét is így adhatjuk meg, pláne ha a paramétereket is programozottan változtathatjuk, valahogy úgy, mint a fenti példában. Nem mellékes az sem, hogy még mindig spártai egyszerűség jellemzi az interpreterünket.

A' propos!
Leesett valakinek, hogy esetünkben a Numeric/Alphabet függvénypárnak csak az egyik tagját kell beépítenünk? Ugyanis, ha nem ez (VAR), akkor értelemszerűen az (SZÁM) lesz az eredmény.

Na de várjunk csak. Olyan biztos ez?

Sajnos egyáltalán nem, ugyanis a programunk most még bármit megeszik, amit felkínálunk neki és olyan - szintaktikailag hibás - sorokat is elfogad, mint ez itt:

LET  230,A      vagy ez  INC  400,0   netán ez  HUP  2021,6X0  vagy ez  HUP  2021       X.
 
   Igen, a valóság az, hogy a programunk így elég körülményesen használható. Miért?
   Mert a gépelés során ejthetünk olyan hibákat, amelyeket az interpreterünk ProgLoad-ere és Tokenizer-e ugyan benyel, de a végrehajtó rész már nem tud a hibával mit kezdeni. Ez pedig bosszantó, mert nekünk kell lokalizáni a hibát, ami egy 120, 150, de még egy 50 soros program esetén is fárasztó és időrabló tevékenység.

Mit lehetne ez ellen  tenni?

   Hát azt, hogy már a feldolgozás során kiszűrjük a hibákat és nem engedjük, hogy azok  begyalogoljanak az interpreterünk közepébe. Ezt ugyanúgy, elég egyszerűen meg tudjuk tenni, mint ahogy az interpreter CASE ágában a paraméter tipusának meghatározásánál tettünk, amikor csekkeltük, hogy a paraméter változó-e vagy szám.

A feldolgozás ugye soronként történik.

   A nulladik token vagy jó, vagy nem. A 'jó' ez esetben azt jelenti, hogy az interpreterünk ismeri a szót (utasítást) és végre tudja hajtani.  Ha nem jó, akkor azt jelezzük (sorszámmal együtt, ami bekerül a kimeneti fájlba (elegancia, ugye ..)) és kilépünk.
Ha jó, akkor a token neve alapján tudjuk, hogy hány paramétere lehet és azok milyenek lehetnek. A LET, INC, DEC esetén például az első param csak betű lehet, szám nem. A második paraméter már árnyalja a helyzetet, hiszen a LET kulcsszó esetén csak szám jöhet szóba, de az INC és DEC mindkét paramétere lehet változó is, szám is. Aggódni nem kell, elég a CASE megfelelő sorában foglalkozni a kérdéssel és csak azzal, ami ott szóba jöhet.   
Az egészhez itt sem kell sokkal több, mint a már említett Numeric/Alphabet függvények. Tulajdonképpen ezek a függvények elegek is, csak kissé ki lesznek egészítve.

Mit is tudnak ezek a függvények?

   Lényegében, egy-egy karakterről állapítják meg annak hovatartozását, tipusát. Na és mi kell nekünk? Hát az, hogy ne csak egy karakterről, hanem egy egész karaktersorról (ez a token)  állapítsa meg ugyanezt. Én most itt a Numeric függvényt átneveztem Numr-nek, az Alphabet függvényt pedig Alfa-nak. De a működésük ugyanaz maradt. A megoldás pedig így fest:

 function Alfa(c: char): boolean;
 begin
   Alfa := Upcase(c) in ['A'..'Z']; end;
 
 function TestAlfa(s : string): boolean;
 var i: byte;
 begin
   TestAlfa := true;
   for i:= 1 to length(s) do
     if Alfa(s[i]) <> true then TestAlfa := false; end;
 
 function Numr(c: char): boolean;
 begin
   Numr := c in ['0'..'9'];  end;
 
 function TestNum(s: string): boolean;
 var i: byte;
 begin
   TestNum := true;
   for i:= 1 to length(s) do
     if Numr(s[i]) <> true then TestNum := false; end;
    
   Tehát, a TestAlfa és a TestNum függvények megkapják bemenetül a vizsgálandó tokent (s nevű string) és annak minden karakteréről kiderítik, hogy betű-e vagy szám-e. Mivel vagy csak betű, vagy csak szám szerepelhet a tokenekben, így a függvények munkája is egyszerű. Ha a token minden karaktere egy féle, akkor true-val, ha különböző (tehát hibás) akkor meg false értékkel térnek vissza.

A jó numerikus token: 1, 25, 658      és a rossz:  1a0, 20u0, b67

A jó alfabetikus token: GOTO, ENDLOOP, PENCOLOR    és a rossz:    GO2O, P3NCOLR, LOOP4

   Összegezve a teendőket. Ha valaki egy igazán használható szintre szeretné felhozni az itt bemutatott interpretert, akkor a bemenő adatok validálását be kell építenie.
Az utasításkészlet ellenőrzéséhez kelleni fog egy szólista, ami lehet konstans, de lehet egy string tömb is.
   Az adatok (script) beolvasása így is, úgy is megtörténik. Az esetleges hibákkal együtt.  Ezzel nem kell foglalkoznunk, hiszen ez nem terheli a processzort számottevően.
Ezután következik a két fő lépés, a soronkénti részekre bontás és a végrehajtás (ez utóbbi maga az interpretálás). A részekre bontás esetén egy sort bontunk fel (Tokenizerrel) és ha ez megvan, akkor érdemes a beolvasottakat ellenőrizni. Ezt még az Interpreter meghívása előtt meg kell tennünk.
   A nulladik TOKEN tartalmát a szólistán végig kell futtatni, hogy van-e a lista valamelyik elemével egyezés. Ha nincs, akkor error + sorszám (ez a Pi) és kilépés.
Ha van egyezés, akkor a nulladik token tartalma (tehát az utasítás) által elvárt paraméterek ellenőrzése jön. Ekkor nem csak a paraméterek jóságát kell vizsgálni, hanem azt is, hogy egyáltalán, megvan-e a kivánt mennyiség.
Ezt a Ti (Token index) változóból tudjuk meg. Ha az utasítás egy paramétert vár el, akkor a Ti értéke 1, ha kettőt, akkor 2 kell, hogy legyen. Ha nincs elvárt paraméter (például az ENDLOOP esetében), akkor a Ti értéke 0.

   Ebből következik, hogy a szólista tömbjét érdemes inkább rekord tömbként megvalósítani, az alábbi prototípussal:

Type
          TInstr = record
          utasitas : string;
          param    : byte;
end;

rLista : Array[0..17] of TInstr;

   Számunkra az a legjobb, ha az interpretálás folyamata már száz százalékban validált, megfelelő méretű és tipusú adatok alapján megy végbe.
Ez a fentieken túl azt is megköveteli, hogy pl, a változó tipusú token alfabetikus legyen és egy karakternél ne legyen hosszabb.
   Ugyanez érvényes a numerikus értékekre is. Ott sem lehet negatív szám, de nem lehet a word tipus értékkészletén túlmutató sem. Tehát, eleve nem állhat ötnél több számjegyből.

Ez sem nehéz feladat. Pl, a számokat tartalmazó tokenek hosszát a TestNum függvényben ellenőrizhetjük, így:

procedure error(s: string);
begin
 Writeln(s); halt;
end;  

If length(s) > 5 then error('too big number in '+intToStr(Pi));  

Vagy  így még jobb:

If StrToInt(TOKEN[1]) > 65535 then  error('too big number in '+intToStr(Pi));
   

Egy kicsivel több, maradhat?

   A hentesnél és a felvágottas pultnál szoktuk hallani ezt a mondatot, pedig itt is előfordulhat hasonló szituáció. Mi van akkor, ha a vizsgált sor a szükségesnél nem kelvesebb, hanem több, hosszabb?  Ilyen esetben két lehetőségünk van. Az egyik, hogy a többlet maradhat, azaz nem veszünk róla tudomást (pláne, ha amúgy szintaktikailag megfel a sor), a másik lehetőség, hogy hibát jelzünk és leáll a feldolgozás. 

   A mostani állapot olyan, hogy azonnal elszállna a program, ha lenne sorvégi fölösleg, ugyanis azt megpróbálná a program bepakolni egy olyan helyre, ami nem létezik. Túlindexelné a TOKEN nevű tömbünket.   Ez a dolog a legegyszerűbben úgy védhető ki, hogy a TOKEN nevű tömböt (03. sor) kicsit túlméretezzük, pl. 0..6-ra. a validálást pedig a megkivántak szerint végezzük el csak.
Persze meg kell jegyezzem,  ez a hibakezelésnek egy elég primitív módja.

Ezúton is köszönet illeti meg NevemTeve kollégát, aki hozzászólásával hathatósan segített még jobban egyszerűsíteni az interpretert.
A változás a 07. sorban lelhető fel, a változók tömbjének deklarációjánál.
Az eredménye, hogy így közvetlenül a változó nevével, magával a karakterrel hivatkozhatunk az értékét tároló tömbelemre, ha írni, ha csak olvasni szeretnénk azt.  Az Ord() függvényt tehát ebben a viszonylatban el lehet felejteni.

Ez a változtatás magával hozta, hogy a 17. és a 20. sorban a VarGet és VarSet függvények bemenő paramétere is Char tipusú és az 50. sorban a változók listájának kiiratását is módosítani szükséges.

Azt talán még érdemes  megemlíteni, hogy a változók a scriptjeinkben nem igényelnek deklarációt, hiszen azok élnek, léteznek anélkül is.
Ha pl. az

INC  C,30

sort begépeljük és futtatjuk a scriptet, azt találjuk, hogy a C felvette a 30 értéket. Ez természetes is, hiszen a C és a többi változó is nullával inicializálódik, él már a scriptünk futtatása előtti pillanatban. 

Egy minimál elvű interpreter (hibakezeléssel):

https://pastebin.com/v5CxhkSV

Kulcs-szavak:

X,Y - canvas poziciók,  U,D,L,R - irányok,  C -  toll szine (0-4), P - Toll vastagsága  (0-4)+1.

Formátum:
X 10
Y 10
P 3
C 1
D 80
R 80
U 80
L 80  

Nincs komment lehetőség, nincs loop és nincsenek változók sem. De azért működik.

..

 

src list 00

Hozzászólások

Szerkesztve: 2021. 07. 21., sze – 13:44

A posztban, a pastebin linken  elérhető minimál rajzgép tekinthető úgy, mint primitív rajzoló függvények kisebb halmaza. 

Ez a szimpla rajzgép magasabb szinten is programozható. Ha valaki kedvet érzez hozzá, írhat egy transpilert, ami a magasabb szintű függvényeket felbontja a minigép által értelmezezhető,  alacsonyabb szintűekre.

Például, a rect utasítás is négy RIGHT, DOWN, LEFT, UP utasításból áll. 
A Goto utasítás pedig  egy X n és egy Y n függvény.