Informatika szigorlat 17-es tétel: Felülről lefelé elemzések 1. Lexikális elemzés A lexikális elemző alapvető feladata az, hogy a forrásnyelvű program lexikális egységeit felismerje, azaz meghatározza a forrásnyelvű kódban a szimbólumok szövegét és típusát. A szimbólikus egységek precíz definíciója reguláris kifejezésekkel adható meg. Emiatt válik külön a szintaktikus elemezőtől, hiszen az nem adható a Chomsky 3-as nyelvosztállyal. Ezek könnyen valósíthatóak meg véges determinisztikus automatákkal. Az elemző létrehozásának menete: 1. Meg kell konstruálni a szimbólikus egységek grammatikájával ekvivalens véges determinisztikus automatát. 2. Ezt kell implementálni. Ez egyszerűen megvalósítható a case utasítás használatával. A lexikális elemző egy szimbólumot a lehető leghosszabbnak tekint. Mindegyik alternatív utasítás addig dolgozza fel a karaktereket, amíg azok az éppen építés alatt álló szimbólum részeként értelmezhetők. A lexikális elemző feladatai közé tartozik a whitespace karakterek (a szóköz és a tab, a sorvége jeleket már source-handler kiszűrte) eltávolítása is. Itt a speciális problémák közé sorolható a kulcsszavak és a standard szavak felismerése. Leggyakoribb megoldás az, ha egy külön táblázatban tároljuk ezeket. Amikor lexikális elemző egy szimbólum beolvasása végére ér, ellenőrzi, hogy ez benne van-e a táblázatban, és ha igen, akkor felismeri ezt, ha nincs benne, akkor határozza meg, hogy milyen lexikális egység. Ha a lexikális elemző egy karaktersorozatnak nem tud egy szimbólumot sem megfeleltetni, akkor azt mondjuk, hogy lexikális hiba van a karaktersorozatban. Általában két hibaelfedő algoritmus egyikét használják: vagy nem foglalkoznak a már beolvasott karakterekkel, az elemzést a következő, még nem vizsgált karakterrel folytatják vagy egyszerűen kihagyják vagy egy tetszőleges karakterrel, általában szóközzel helyettesítik az illegális karaktert. Lexikális elemző generátor a lex nevű program, ez a reguláris kifejezésekből elkészíti a lexikális elemző programot. 1
2. LL elemzések A felülről lefelé elemzések úgy működnek, hogy a nyelvtan kezdőszimbólumából kiindulva legbaloldalibb helyettesítéseket alkalmazva próbál eljutni az elemezendő szöveghez. Az LL elemzések ezen belül visszalépés nélkül oldja meg a feladatot. Az LL(k) elemzések, amiatt, hogy ne kelljen visszalépést végrehajtania, k szimbólumot kell hogy előreolvasson. Def.: A G grammatikát egyszerű LL(1) grammatikának nevezzük, ha ε - mentes, és ha minden A nemterminális szimbólumra a helyettesítési szabályok különböző terminális szimbólummal kezdődnek, azaz k 1 -re A a 1 α 1 a 2 α 2... a k α k, ahol a i a j, ha i j. Az elemzés állapotait az (x, β, y) hármassal jelöljük, ahol x a még nem elemzett szöveg, β egy vermet, y pedig egy listát jelent. Az elemezendő szöveg végét # jellel megjelöljük, és a helyettesítési szabályokat a felsorolásuk sorrendjében megszámozzuk. Az elemzéshez egy verem használható. A verem alját is megjelöljük a # jellel. Ebben a veremben lesz az aktuális mondatforma, a kezdeti tartalma legyen S (a nyelvtan kezdőszimbóluma). A verem tetején lévő szimbólumot fogjuk összehasonlítani az input szöveg következő, még nem elemzett szimbólumával. Az alkalmazott szabályok sorszámát mindig feljegyezzük az y listába. Ezzel lehet majd az elemzés végén a szintaxisfát felépíteni. Az elemzést egy táblázat segítségével fogjuk elvégezni. A tábla sorai a verem tetején lévő szimbólumot, az oszlopai a következő elemezendő szimbólumot jelölik. A tábla elemei: pop ha X = a accept ha X = # és a = # M(X, a) = (aα, i) ha X aα az i-dik helyettesítési szabály error egyébként Az elemzés állapotátmenetei: a kezdőállapot legyen (x#, S#, ε), és az elemzés sikeresen fejeződik be, ha az elemző az (#, #, y) állapotba kerül. Ha nem elemzett szöveg az, és a verem tetején az X áll, akkor az állapotátmenetek a következők: (z, α, y) ha M(X, a) = pop OK (az, Xα, y) ha M(X, a) = accept (az, βα, yi) ha M(X, a) = (β, i) error ha M(X, a) = error 2
Bővebb nyelvosztályra alkalmazható az elemzés, ha nem tesszük azt a kikötést, hogy különböző terminális jellel kezdődjenek a szabályok. F IRST (α) := {a α aβ} Def.: A G ε-mentes grammatikát ε-mentes LL(1) grammatikának nevezzük, ha minden A nemterminális szimbólumra és k > 1-re: A α 1 α 2... α k esetén F IRST (α i ) F IRST (α j ) = Az elemzés ugyanúgy működik mint az előző esetben, az állapotmenetek is megegyeznek, csak kis változás érinti a táblázat meghatározást: pop ha X = a accept ha X = # és a = # M(X, a) = (α, i) ha X α az i-dik helyettesítési szabály és a F IRST (α) error egyébként F IRST k (α) := {x α x, ha x < k, α xβ, ha x = k} Def.: A G grammatika LL(k) grammatika (k 0), ha: waβ wα 1 β wx waβ wα 2 β wy és F IRST k (x) = F IRST k (y) esetén α 1 = α 2. Def.: A G nyelvtant erős LL(k) (k 0) grammatikának nevezzük, ha waβ wα 1 β wx vaϑ vα 2 ϑ vy és F IRST k (x) = F IRST k (y) esetén α 1 = α 2. Ezekben az esetekben a mindig legkisebb ilyen k értéket értjük ez alatt. A k = 1 esetben a két nyelvosztály megegyezik. Ezekhez a nyelvekhez a fentiekhez hasonlóan létezik egy táblázatot használó módszer. Def : F OLLOW k (β) = {x αβγ és x F IRST k (γ)} k = 1 esetben a táblázat elemei a következő módon határozhatók meg: pop ha X = a accept ha X = # és a = # M(X, a) = (α, i) ha X α az i-dik helyettesítési szabály és a F IRST (α) (α, i) ha X α az i-dik helyettesítési szabály és a F OLLOW 1 (X) error egyébként Az elemzés fenti említett módon végezhező. A problémát ekkor az jelenti, hogy meg kell határozni a F IRST 1 (α) és a F OLLOW 1 (A) halmazokat. Erre két módszer lehetséges: 3
Az LL(1) grammatika szabályaiból közvetlenül kaphatunk egy elemző programot, ha a rekurzív leszállás módszer ét használjuk. Ennek a lényege az, hogy a nyelvtan nemterminális szimbólumaihoz eljárásokat rendelnek, és az elemzés közben a rekurzív procedúrahívásokon keresztül a programnyelv implementációja valósítja meg a veremkezelést. procedure accept(szimbolum); if aktualis-szimbolum=szimbolum then kovetkezo-szimbolum else error(...) A grammatika minden nemterminális szimbólumához rendeljünk hozzá egy procedúrát. Az A szimbólumhoz tartozó procedúra a következő: procedure A; T(A) ahol T(A) az A-ra vonatkozó helyettesítési szabályok határozzák meg: 1. Az A a szabályhoz rendelt program legyen az accept(a). 2. Az A B szabályhoz rendeljük hozzá a B procedúrahívást. 3. Az A X 1 X 2... X n szabályhoz tartozzon a következő blokk: T(X1); T(X2);... T(Xn) 4. Ha az A X 1 X 2... X n szabály ε-mentes, akkor T(A) legyen case aktualis-szimbolum of first(x1): T(X1); first(x2): T(X2);... first(xn): T(Xn); 4
5. Ha az előzőhöz hozzávesszük az ε-szabályt is, akkor a fenti elágazás végére hozzá kell írni follow(a): -t is. 3. Szemantikus elemzés Szemantikus elemzés alatt statikus szemantikus elemzést értünk, azaz olyan tulajdonágokat elemezzen a fordító, mint például változók deklarációja, hatásköre, láthatósága, alprogramok formális és aktuális paraméterei közötti kompatibilitás, stb.. Ezek a tulajdonságok nem írhatóak le környezetfüggetlen nyelvtanokkal. Az elemzésre az a módszer terjedt el, hogy az egyes szemantikus tulajdonságok vizsgálatára önálló programokat írnak. Ezekhez sokszor kell használni a szimbólumtáblát. A legegyszerűbb módszer az, ha a nyelvtan szabályait kiegészítjük speciális jelekkel, akciószimbólumokkal, amik azt jelentik, hogy egy szemantikus rutint végre kell hajtani az elemzéskor. Ha @s egy akciószimbólumot jelöl, akkor az α@sβ x levezetésben a @s azt jelenti, hogy a @s sorrakerülése esetén az s szemantikus elemző programelemet kell meghívni, és az elemzés csak ennek a programelemnek a lefutása után folytatódhat. Például: <változó> <kifejezés>@checktype. Természetesen nem kell minden szabályt ílymódón bővíteni, például: <utasítássorozat> <utasítás><utasítássorozat>. Ha a grammatika szabályait a szemantikus elemzés céljából akciószimbólumokkal egészítjük ki, akkor a grammatikát fordítási grammatikának nevezzük. Az ilyen jellegű grammatikákhoz a rekurzív leszállás módszerét könnyen lehet módosítani. Ha a fordítási grammatika egy szabálya A @a 1 X 1 @a 2 X 2... X n @a n+1 ahol @a i egy akciószimbólum vagy ε, akkor ehhez a szabályhoz tartozó program legyen a következő: procedure A; [a1;] X1; [a2;] X2;... Xn; [an+1] 5
A szögletes zárójel jelzi azt, hogy, ha@a i = ε, akkor az eljárásnak nincs ilyen sora. Ezt a módszert hívják implicit szemantikus vermet kezelő szemantikus elemzésnek. Az AT G = (T G, A, V, R, C) ötöst attribútúm fordítási grammatikának nevezzük, ahol - T G egy fordítási grammatika - A az attribútumok véges halmaza - V az attribútumértékek halmaza - R a szemantikus szabályok halmaza - C a logikai feltételek halmaza. Az X szimbólum egy attribútumát szintetizáltnak nevezzük, ha értékét egy szemantikus függvény abban az esetben határozza meg, amikor az X szimbólum egy helyettesítési szabály baloldalán áll, ha pedig a jobb oldalon áll, akkor azt örökölt attribútumnak nevezzük. Tehát az információt egy szintaxisfában a szintetizált attribútumok alulról felfelé, az örököltek pedig felülről lefelé és szinten belül továbbítják. Egy ATG-t L-attribútum fordítási grammatikának nevezünk és L AT G- vel jelölünk, ha minden A X 1 X 2... X n helyettesítési szabályra az attribútumok kiszámítási sorrendje a következő: J (A), J (X 1 ), P(X 1 ), J (x 2 ),..., P(X n ), P(A), ahol J (X i ): örökölt attribútum kiértékelés, és P(X i ): szintetizált attribútum kiértékelés. Ilyen típusú grammatikával elérhető, hogy a szemantikus elemzést a szintaxisfa építésével párhuzamosan lehessen elvégezni. Az L-ATG grammatikák jól használhatók a felülről-lefelé elemzéseknél, például az LL(1) elemzőkben. 4. Kódgenerálás Kódgenerálás alatt azt értjük, hogy a már elemzett magasszintű programból, hogyan lehet alacsonyszintű assembly programot generálni. 5. Szimbólumtábla- és memóriakezelés A szimbólumtábla egy olyan táblázatnak tekinthető, amelyben egy sor egy szimbólum programbeli jellemzőit tartalmazza. Ezek az attribútumok elsősorban a programnyelvtől függenek, de a leggyakrabban tárolt attribútumok a következők: 6
a szimbólum neve a szimbólum definíciójának adatai a szimbólum típusdescriptora a szimbólum tárgyprogrambeli címe annak a forrásnyelvi sornak a sorszáma, amelyben a szimbólumot definiálták azoknak a soroknak a sorszáma, amelyekben hivatkozás történik a szimbólumra a szimbólum ábécé-sorrendbeli láncolási címe Két alapvető művelete van a szimbólumtáblának: beszúrás és keresés. Blokkstruktúrájú nyelvek esetén szükség van még a set és a reset műveletre. A set egy blokkhoz tartozó altáblát nyit meg, a reset pedig a blokk végén törli ezt. Dinamikus memóriakezelés esetén a programelemek méretét és az egyes példányok darabszámát csak végrehajtási időben kell megismerni. Ezt általában blokkstruktúrájú programok végrehajtásakor alkalmazzuk. Mivel minden blokkhoz egy önálló adatmező tartozik a run-time verem jól használható az adatelemek elhelyezésére, egy blokkba való belépéskor a blokk adatait a verembe tesszük, a blokkból való kilépéskor töröljük. Az i-dik blokkhoz tartozó adatelemeket az AR i aktivációs rekord tartalmazza, az aktivációs rekordban a blokk lokális változói, paraméterei és a blokkból látható változókra mutató pointerek vannak. Az aktivációs rekord három részből áll, a lokális változók területéből, a display területből(statikus pointerek), és a paraméter területből. A blokk lokális változóit a blokk szintszámából és a blokkon belüli relatív címből álló kettőssel ábrázolhatjuk. Célszerű a display terület bővítésekor a megelőző aktivációs rekordra mutató pointert használni. 7