Bu qanday ishlaydi: Kompilyator (1-qism)

OS yoki kompilyator yaratishni hech bo'lmaganda bir marta o'ylab ko'rmagan dasturchi dasturchi emas degandi bir tanishim. Bu gapda jon bor, chunki OS va kompilyatorlar murakkab dasturlar sirasiga kiradi. Kompilyator haqida o'zim bilmagan holda 15 yil oldin o'ylashni boshlagandim. O'shanda maktabda Pravetz-8 kompyuterida o'tirib Basic dagi kodni qanday qilib tez ishlatish, assemblerga o'girish mumkinmikan deb bosh ham qotirgandim (Ma'lumot: Pravetz-8 kompyuteri O'zbekiston-Bolgariya hamkorligida ishlab chiqarilgan, ~4MHz chastotali adashmasam, 6502 mikroprosessor asosidagi, 64K operativ hotiraga ega kompyuter edi). ROM dagi «quyma» Basic interpretatori shu qadar sekin ishlardiki, ekranga biror shaklni chizilsa uni nuqtama nuqta kuzatib o'tirish mumkin edi.
Hullas hozir gap bu haqida emas. Demoqchi bo'lganim kompilyatorlar haqida. Mana o'sha paytlardan beri orzu bo'lgan narsa bugunga kelib ancha osonlashib qoldi. Sababi dasturlash tillari rivojlangan sari ushbu yo'nalishda turli yangiliklar qilindi, har hil algoritmlar topildi. Kompilyatorlar yaratish jarayonini yengillashtirish uchun «kompilyatorlar kompilyatori», parserlar, leksik va sintaktik analizatorlar kabi tayyor yordamchi dasturlar ishlab chiqarildi. Shular sababli hozir dasturlash tillari, kompilyatorlar minglab desak ham hato bo'lmaydi. Ushbu maqolada kompilyatorlar, ularni dasturlash kabi o'zim bilgan ma'lumotlarimni gapirib o’tmoqchiman.

Hammamizga ma’lumki kompyuter yoki har qanday dastur bilan ishlovchi qurilmalar uchun dasturlar biror bir dasturlash tillarida yoziladi. Bunday tillar hozirda juda ko’p. C, C++, Delphi, Ruby, Perl, Python, Java va shu kabi universal dasturlash tillaridan tashqari maxsus biror yo’nalish yoki platformaga mo’ljallangan tillar, skript tillar ham bir talay. Dastur tuzish uchun dastur tuzilayotgan qurilma ichki tuzilmasini hisobga olib yuqoridagi tillardan birida dastur matni yoziladi. So’ngra bu dastur ishlashi uchun dastur matnini u mo’ljallangan qurilmaning “tiliga”, ya’ni prosessor instruksiyalari (buyruqlari) ketma-ketligiga tarjima qilish kerak bo’ladi. Bu vazifani kompilyator dasturi bajaradi.
Kompilyator – biror dasturlash tilida yozilgan dastur (matni)ni o’sha til qoidalariga muvofiq boshqa bir tilga tarjima qiluvchi dastur. Natijaviy til mashina kodi, oraliq kod (p-kod, baytkod) yoki boshqa quyi/yuqori darajali dasturlash tillari bo’lishi mumkin. Ushbu tarjima jarayoni kompilyasiya deyiladi.
Agar kompilyasiya jarayonida dastur mashina kodiga tarjima qilingan bo’lsa, hosil bo’lgan kod o’sha qurilma/muhitda bevosita ishlayveradi. Agar dastur oraliq kodga tarjima qilingan bo’lsa, u holda bu kodni ishlatish uchun Virtual Mashina kerak bo’ladi. Virtual mashina – oraliq kodda ifodalangan dasturni qadamlab mashina kodiga tarjima qiladi va dasturni ishlatadi.
Shu joyda “Oraliq kodga o’tkazishni nima keragi bor, dasturni to’g’ridan to’g’ri mashina kodiga o’girish yaxshimasmi?” degan savol tug’ilishi tabiiy. Oraliq kod birinchi navbatda dasturning kross platformaliligini (dastur o’zgarishsiz yoki qisman o’zgartirish orqali boshqa platformalarda ham ishlayverishga yaroqli bo’lsa bunday dastur kross platformali deyiladi) ta’minlaydi. Har bir platforma, OS o’zining ichki arxitekturasi jihatidan bir biridan farq qiladi, shunday ekan bir muhitga moslab yozilgan mashina tilidagi kod ikkinchi boshqa bir muhitda ishlamasligi tabiiy. Oraliq kodlarda yozilgan dasturlar huddi shu muammoni hal qilishda yordam beradi. Bunda oraliq kodni ishlatish uchun kerak bo’ladigan Virtual Mashina turli muhitlar, OS lar uchun mavjud bo’lsa yetarli, ya’ni har bir dasturni boshqa muhitga moslash shart emas, faqatgina Virtual Mashinani kerakli muhitlar uchun moslansa yetarli. Bundan tashqari oraliq kod dasturni tahlil qilish (debug) jarayonini yengillashtiradi ham. Oraliq kod hosil qiluvchi tillarga misol qilib, Java, Python, Ruby kabilarni keltirish mumkin.

Xo’sh kompilyator qanday ishlaydi?

Har qanday tillar o’zining grammatikasi yoki sintaksisiga ega bo’ladi. Masalan, o’zbek tilida to’g’ri yozilgan gapda kesim egadan keyin kelishi kabi qoida ham til sintaksisining bir bo’lagidir. Dasturlash tillarida ham tabiiy tillar singari o’zining qat’iy qonun-qoidalari bo’ladi. Kompilyasiya jarayoni huddi shu qoidalarga asoslangan holda amalga oshiriladi.
Erkin kontekstli dasturlash tillari uchun kompilyatorlar odatda asosan 5 bosqichdan iborat bo’ladi:

• Preprosessor
• Leksik tahlil
• Sintatktik tahlil (parser)
• Semantik tahlil
• Tarjima qilish

Har bir bosqichni batafsil ko’rib chiqamiz.
Preprosessor – dastur matnini yig’ish vazifasini bajaradi. Misol uchun, c++ da include yordamida kodga qo’shilgan qo’shimcha fayllar o’qilib, asosiy faylni boshiga qo’shiladi va yaxlit bir fayl ko’rinishiga keltiriladi. Va ba’zi kompilyasiyadan oldingi shartli amallarni bajaradi. Ushbu bosqich qolgan bosqichlardan eng soddasi bo’lib, bu bosqichni tashlab ketsam ham bo’laversa kerak.
Leksik tahlil – dastur matnini tilning alifbosiga mos ravishda bo’laklarga, “so’z”larga ajratish vazifasini bajaradi. Natijada identifikatorlar, literallar, bo’sh joylar, amallar va boshqa bo’laklar hosil bo’ladi. Masalan, deylik, o’zbek tiliga asoslangan qandaydir dasturlash tilida
agar (i<j) bo’lsa i = 1;

ko’rinishidagi matn tahminan quyidagicha bo’laklarga ajratiladi.
№	Bo’lak	    Turi (token)
---------------------------------------------------------------------------------------
1	agar	    identifikator (kalit so’z)
2		    bo’sh joy (probel, tab belgisi yoki boshq.). Til alifbosida bo’sh    
                    joyni vazifasi bo’lmasa, barcha bo’sh joylarni tushirib qoldirish
                    mumkin. 
3	(	    operator
4	i	    identifikator
5	<	    operator
6	j	    identifikator
7	)	    operator
8	bo’lsa	    identifikator (kalit so’z)
9	i	    identifikator
10	=	    operator
11	1	    sonli literal
12	;	    ajratgich
13		    dastur oxiri (mahsus token)
Bo’laklash bosqichini dasturlash ham murakkab ish emas. Buning uchun har bir bo’lak qanday belgilar to’plamidan iboratligi, qaysi belgidan keyin qaysi belgi kelishi kabi belgilashlarlarni aniqlab olishimiz kerak. Shu joyda bir narsani aytib o’tish lozimki, hozirda agar biror bir kompilyator yozmoqchi bo’lsangiz uni 0 dan yozish ham shart emas. Bunda Sintaktik analizator (parserlar) va Kompilyatorlar kompilyatori kabi bir qancha tayyor dasturlardan foydalanish ishni ancha yengillashtiradi. Bu dasturlar kirishiga tilning formal yozuvdagi ko’rinishi berilsa, leksik tahlil va sintaktik tahlil qadamlarni avtomatik bajarib beradigan, qisman tarjima bosqichini ham bajaradigan kod hosil qilib beradi.
Tilning formal yozuvi deganda tilning alifbosi, qonun qoidalarini ko’rsatuvchi yozuvga aytiladi. Masalan, yuqoridagi
agar (i<j) bo’lsa i = 1;

dagi agar buyrug’i sintaksisi quyidagicha bo’lishi mumkin:
<Agar> ::= agar (<Ifoda>) bo’lsa <Buyruqlar>

Bunda “agar”, “bo’lsa” so’zlari terminallar, <Agar>, <Buyruqlar>, <Ifoda> esa nonterminallar deyiladi. “::=” belgisi esa “shundan iborat” ma’nosini beradi. Ya’ni <agar> qoidasi “::=” belgisidan keyingilarga teng ma’nosida. O’z navbatida <Ifoda> va <Buyruqlar> uchun ham kerakli qoidalar ko’rsatiladi. Bundan keyin terminallarni so’zlar, nonterminallarni qoidalar deb atayman.
Tilning sintaksisini ko’rsatishda Bekus-Naur yozuvidan keng foydalaniladi.
Bekus-Naur yozuvida (aniqrog’i, uning kengaytirilgan variantlaridan birida) til alifbosi Belgilar to’plami, So’zlar va Qoidalar degan 3 ta element yordamida ifodalanadi. Belgilar to’plami bu biror bir So’z qanday belgilardan tashkil topishini bildiradi. Masalan Identifikator degan So’z A dan Z gacha lotin harflari, 0 dan 9 gacha raqamlar va “_” belgisidan iborat bo’lishi mumkin degan shart aynan Belgilar to’plami yordamida ko’rsatiladi.
Belgilar to’plami:
{Harflar}             = ABCDEFGHIJKLMNOPQRSTUVWXYZ
{Raqamlar}            = 0123456789
{Identifikator boshi} = {Harflar} [_]
{Identifikator oxiri} = {Raqamlar} [_]

Bu yerda {Harflar} nomli belgilar to’plami A dan Z gacha lotin harflaridan biridan iborat bo’lishi mumkinligi, {Raqamlar} esa 0 dan 9 gacha raqamdan biri ekanligi, {Identifikator boshi} esa {Harflar} dan va “_” belgisidan tuzilganligi va {Identifikator oxiri} esa {Raqamlar} va “_” belgisidan iborat ekanligi ko’rsatilyapti. Belgilar to’plamini berishda “+” belgisi belgilar to’plamiga yangi belgini qo’shish, “-“ belgisi esa keraksiz belgini chiqarib tashlashni bildirishi mumkin. Identifikator boshi va oxiri deb ikkita bo’lakka bo’lishdan maqsad esa, identifikatorlar nomi faqat harf yoki “_” belgisidan boshlanishi mumkinligidan kelib chiqdi.
Belgilar to’plami qay tartibda kelishi So’zlarda ifodalanadi. Yuqoridagi belgilar to’plamidan foydalanib Identifikatorni qanday berilishini ko’ramiz:
Identifikator = {Identifikator boshi} {Identifikator oxiri}*

Bu yerda “*” belgisi uning oldida kelgan belgilar to’plami yoki belgining 0 yoki undan ko’p marta, “+” belgisi esa 1 yoki undan ko’p marta takrorlanishi mumkinligini ko’rsatadi (huddi Regular Ifodalardagi kabi). Yana misol:
ButunSon       = {Raqamlar}
HaqiqiySon     = {Raqamlar}* “.” {Raqamlar}
MantiqiyQiymat = “rost” | “yolg’on”

Qo’shtirnoq ichidagi yozuvlar ularni So’zda qanday bo’lsa shunday holicha kelishini (leksema), “|” belgisi esa “yoki” amalini bildiryapti.
Va nihoyat So’zlarning qay tartibda kelishi Qoidalar yordamida beriladi:
<Agar>          ::= “agar” <Ifoda> “bo’lsa” <Buyruqlar>
                  | “agar” <Ifoda> “bo’lsa” <Buyruqlar> “aks holda” <Buyruqlar>
<Qiymat berish> ::= Identifikator “=” <Ifoda>
...
<Buyruqlar>     ::= <Agar buyrug’i> “;”
                  | <Takror buyrug’i> “;”
                  | <Toki buyrug’i> “;”
                  | <Tanlash buyrugi> “;”
                  | <Qiymat berish> “;”
                  | <Buruqlar>
                  |
...

E’tibor bering, oxirgi Buyruqlar qoidasini berishda rekursiyadan foydalanilyapti, bu esa buyruqlarni ichma ich va ko’p marotaba qaytarilishini ifodalash uchun kerak bo’ladi. Ko’rinib turibdiki, bunday yozuvlardan foydalanib har qanday tilning sintaksisini, alifbosini bemalol ko’rsatish mumkin. Bekus-Naur yozuvidan tashqari yana bir nechta shu kabi tilni “qolipini” tushuntirishda qo’llaniladigan yozuvlar mavjud.
Kompilyator yaratishdan oldin tilning yuqoridagi kabi sintaksisini puxta o'ylab olish lozim.
Endi, keling yaxshisi leksik tahlil jarayonini amalda ko'ramiz. Misol uchun, berilgan matnni Identifikator, ButunSon, HaqiqiySon kabi so'zlarga ajratuvchi dastur tuzamiz. Buning uchun C# da yangi konsol dastur ochib unda Lexer nomli klassni yozamiz. Bunda so'zlarni saqlash uchun Token nomli qo’shimcha klass ham kerak bo’ladi:
// Ushbu klass so'zni saqlash uchun
public class Token
{

    // mumkin bo'lgan so'zlar turi
    public enum TokenType
    {

        UNKNOWN,            // noma'lum so'z
        IDENTIFIER,         // identifikator
        OPERATOR,           // operator
        NUMLITERAL,	    // sonli literal
        STRINGLITERAL,      // matnli literal
        WHITESPACE,         // bo'sh joy
        DELIMITOR,	    // yangi qator
        EOP		    // matn oxirini belgilash uchun

    }

    // qo'shimcha tip uchun
    public enum DataType
    {
        INTEGER,            // butun son
        FLOAT               // haqiqiy son
    }

    public string Text;     // so'z matni
    public TokenType Type;  // so'z turi
    public DataType SubType;
    public int TextPos;     // so'zni dastur matnidagi pozisiyasi

    public Token()
    {
        this.Text = "";
        this.TextPos = 0;
        this.Type = TokenType.UNKNOWN;
    }
}

// Lexer
public class Lexer
{

        // konstantalar
    const char CHAR_NULL = '\0';

    // o'zgaruvchilar
    string text;
    int position;

    // konstruktor
    public Lexer()
    {
        this.position= 0;
        this.text= "";
    }

    // konstruktor
    public Lexer(string text)
    {
        this.text= text;
        this.position= 0;
    }

    // dastur matnini berish/olish
    public string Text
    {
        get { return this.text;  }
        set { this.text = value; }
    }

}

Lexer klassimizga berilgan matnni belgilab o’qishga yordam beruvchi funksiyalarni qo’shamiz:

    // joriy pozisiya
    public int Position
    {
        get { return this.position; }
        set { this.position = value; }
    }

    // joriy belgini aniqlash
    private char CurrentChar()
    {

        return (position < text.Length ?
                Convert.ToChar(text.Substring(position, 1)) : CHAR_NULL);
    }

    // joriy belgidan keyingi belgini aniqlash
    private char NextChar()
    {

        return (position + 1 < text.Length ?
                Convert.ToChar(text.Substring(position + 1, 1)) : CHAR_NULL);
    }

    // joriy belgidan oldingi belgini aniqlash
    private char PrevChar()
    {

        return (position - 1 >= 0 ?
            Convert.ToChar(text.Substring(position - 1, 1)) : CHAR_NULL);
    }

    // keyingi belgiga o'tish
    private void SkipChar()
    {
        position++;
    }


Vanihoyat asosiy qism, so'zlarni “tanish” funksiyasini yozamiz:
// joriy so'zni o'qish
    // skipWhitespaces - bo'sh joylarni tashlab ketish
    public Token ReadToken()
    {
            
        string result = "";
        Token tok=new Token();

        // joriy belgini o'qish
        char ch = CurrentChar();

        // joriy belgi bo'sh joy bo'lsa
        // bo'sh joylar ketma-ketligini o'qiymiz
        if (IsWhiteSpace(ch)) 
        {

            do 
            {

                SkipChar();
                ch = CurrentChar();

            } while (IsWhiteSpace(ch));

        }

        // agar matndagi oxirgi belgi bo'lsa (EOP)
        if (ch == CHAR_NULL) {
                
            tok.Type = Token.TokenType.EOP;

        // agar joriy belgi harf yoki _ belgisi bo'lsa
        // demak identifikatorni o'qiymiz
        } else if (char.IsLetter(ch) || (ch == '_')) {
                
            do 
            {

                ch = CurrentChar();

                if (char.IsLetter(ch) || char.IsDigit(ch) || (ch == '_'))
                    result += ch;
                else
                    break;

                SkipChar();

            } while (true);

            tok.Type = Token.TokenType.IDENTIFIER;
            tok.Text = result;

        // joriy belgi " belgisi bo'lsa
        // matnli literalni o'qiymiz
        } else if (ch =='"' ) {

            do 
            {

                SkipChar();
                ch = CurrentChar();
                result += ch;

            } while (ch != CHAR_NULL && ch != '"');

            if (ch == '"')
            {

                SkipChar();
                tok.Type = Token.TokenType.STRINGLITERAL;
                tok.Text = result.Substring(0,result.Length-1);

            }
            else Console.WriteLine("HATO: Qo'shtirnoq yopilmagan!");

        // qator va buyruq ajratuvchi belgilarni o'qiymiz
        } else if (ch == ';' ) {

            do
            {

                result += ch;
                SkipChar();
                ch = CurrentChar();

            } while (ch == ';');

            tok.Type = Token.TokenType.DELIMITOR;
            tok.Text = result;

        // sonli literalni o'qiymiz
        } else if (char.IsDigit(ch) || (ch == '.' && char.IsDigit(NextChar()))) {
            
            bool hasPoint = false;
                
            do
            {

                ch = CurrentChar();
                    
                if (char.IsDigit(ch))
                    result += ch;
                else if (ch == '.' && !hasPoint)
                {
                    result += ch;
                    hasPoint = true;
                }
                else
                    break;

                SkipChar();

            } while (true);
                
            if (PrevChar() == '.') 
                    Console.WriteLine("HATO: Son noto'g'ri kiritilgan!");

            // mana bu yerda sonli literalni Butun, Haqiqiy kabi
            // turlarga ajratuvchi qism bo'lishi mumkin, ya'ni
            // sonli literal "." bor yo'qligi va qiymatining
            // katta kichikligiga qarab yana kichik turlarga 
            // ajratilishi mumkin
            // ...

            tok.Type = Token.TokenType.NUMLITERAL;
            tok.Text = result;

        // operatorlar
        } else if (IsOperator(ch)) {

            result += ch;
            SkipChar();
            tok.Type = Token.TokenType.OPERATOR;
            tok.Text = result;

        // noma'lum belgi
        } else {
                
            result += ch;
            SkipChar();
            tok.Type = Token.TokenType.UNKNOWN;
            tok.Text = result;

        }

        return tok;
    }

Yordamchi funksiyalar:

    private bool IsWhiteSpace(char ch)
    {
            
        string pattern = " \t\n\r";
        return (pattern.IndexOf(ch) >= 0 ? true : false);

    }

    private bool IsOperator(char ch)
    {
            
        string pattern = "+-*/()=";
        return (pattern.IndexOf(ch) >= 0 ? true : false);

    }

Lexer ni ishlashini tekshirish uchun asosiy klassdagi main funksiyaga quyidagini yozamiz:
static void Main(string[] args)
{

    int n = 0;
    string result;
    string text = Console.ReadLine();
    Token tok;
    Lexer lex = new Lexer( text);

    do // takrorla
    {
                
        n++;
        tok = lex.ReadToken();

        // natijani formatlash va ekranga chiqarish
        result = n.ToString().PadRight (3) ;
        result += (" (" + tok.Type.ToString() + ")").PadRight (18);
        result += tok.Text;
        Console.WriteLine(result);

    } while (tok.Type!=Token.TokenType.EOP ); // toki birorta so'z qolmaguncha

}


Dasturni ishlatamiz va kirishga quyidagicha matn kiritamiz:
A1 “bu matn” 12.5        99
Natija:
1. (IDENTIFIER)      a1
2. (STRINGLITERAL)  "bu matn"
3. (NUMLITERAL)      12.5
4. (NUMLITERAL)      99
5. (EOP)

Ushbu maqolachani davomida kompilyasiyaning keyingi bosqichi — sintaktik tahlil haqida fikr almashamiz.

Dastur kodi: Compiler.zip

P.S. Mavzuni rejasini o'ylamasdan yozilgani uchun ba'zi joylar aralash yoki tushunarsiz bo'lib qolgan bo'lishi mumkin. Oxirigacha oq'iganlarga tashakkur :)

1 комментарий

shranet
Shu kompilyatorni man ham 5 yillar oldin rosa qiziqqandim, lekin u paytlar bunaqa yozilgan maqolalar yo'q edi. Undan tashqari ruschadan ham xabarim yo'q edi. Qolganlarini kutamiz.
0