コンパイラ理論の紹介:C#を使用したPascalの字句解析

前書き



最近、プログラミングの初心者のほとんどは、Java、Python、C#などの高レベルの言語、またはガベージコレクターや既製のデータ構造などの形式で「紳士のセット」を含むその他の言語から始めます。もちろん、このアプローチには利点がありますが、原則として、言語の既製の機能を使用する初心者の開発者は、最も重要なこと、つまり操作と実装の構造とメカニズムを見逃しています。



メモリの割り当てやコードの解釈方法の詳細については説明しませんが、逆に、コンパイラ自体、つまり字句アナライザについて説明し、C#で実装してみます。圧倒的多数は、私たちが分析する言語を知っています-それはパスカルです。



字句アナライザーは、さらなる処理のためにトークンを強調表示する役割を担うコンパイラーの「レイヤー」の最初のものです。



lexemeは、私たちの言語を表す辞書の最小単位です。サービスワード、演算子、識別子などはトークンとして機能します。



実装



構造の説明



言語の正式な説明は、2つの配列に格納されます。最初の-関数ワードと2番目の-区切り文字と見つかったトークンのリスト



private string[] Words = { "program", "var", "integer", "real", "bool", "begin", "end", "if", "then", "else", "while", "do", "read", "write", "true", "false" };
private string[] Delimiter = { ".", ";", ",", "(", ")", "+", "-", "*", "/", "=", ">", "<" };
public List<Lex> Lexemes = new List<Lex>();




lexeme自体がキーを格納します。このキーは、タイプメンバーシップ(サービスワード、演算子、識別子、番号)、トークンのID、および値自体を決定するために使用されます。



class Lex
{
    public int id;
    public int lex;
    public string val;

    public Lex(int _id, int _lex, string _val)
    {
        id = _id;
        lex = _lex;
        val = _val;
    }
}


トークンを処理するための最良の解決策は、ステートマシンです。これにより、不要なifが削除され、ループを簡単に変更できるようになります。Sは初期状態、NUM、DLM、ASGN、IDは対応するタイプのトークンの状態、ERはエラーに使用され、FINは最終状態に使用されます。



private string buf = ""; //    
private char[] sm = new char[1];
private int dt = 0;
private enum States { S, NUM, DLM, FIN, ID, ER, ASGN, COM } //  state-
private States state; //   
private StringReader sr; //    


主なメソッドは、配列内のトークンを検索し、そのIDと値をタプルで返すSearchLex(はい、タプルも便利です)と、新しいトークンを辞書に追加するPushLexです。




private (int, string) SerchLex(string[] lexes)
{
    var srh = Array.FindIndex(lexes, s => s.Equals(buf)); 
    if (srh != -1)
        return (srh, buf);             
    else return (-1, "");
}

private (int, string) PushLex(string[] lexes, string buf)
{
    var srh = Array.FindIndex(lexes, s => s.Equals(buf));
    if (srh != -1)
        return (-1, "");
    else
    {
        Array.Resize(ref lexes, lexes.Length + 1);
        lexes[lexes.Length - 1] = buf;
        return (lexes.Length - 1, buf);
    }
}


アルゴリズムの実装



最初のステップは、ループの終わり(FIN状態)を決定し、初期状態を実装することです。



sr = new StringReader(text); //    
while (state != States.FIN)
{
    switch (state)
    {
        case States.S:
            if (sm[0] == ' ' || sm[0] == '\n' || sm[0] == '\t' || sm[0] == '\0' || sm[0] == '\r' )
                GetNext();
            else if (Char.IsLetter(sm[0]))
            {
                ClearBuf();
                AddBuf(sm[0]);
                state = States.ID;
                GetNext();
            }
            else if (char.IsDigit(sm[0]))
            {
                dt = (int)(sm[0]-'0');
                GetNext();
                state = States.NUM;
                
            }
            else if (sm[0] == '{')
            {
                state = States.COM;
                GetNext();
            }
            else if (sm[0] == ':')
            {
                state = States.ASGN;
                ClearBuf();
                AddBuf(sm[0]);
                GetNext();
            }
            else if (sm[0] == '.')
            {
                AddLex(Lexemes, 2, 0, sm[0].ToString());
                state = States.FIN;
            }
            else
            {
                state = States.DLM;
            }
        break;
    }  
}


GetNextメソッドを使用すると、文字列の次の文字であるClearBufをそれぞれ取得して、トークンを格納しているバッファをクリアできます。



private void GetNext()
{
    sr.Read(sm, 0, 1);
}


2つの別々の演算子で構成される「:=」割り当て演算子には特に注意を払う必要があります。この演算子を定義する最も簡単な方法は、条件を追加して中間値をバッファーに書き込むことです。このために、別のASGN状態が実装されました(翻訳では、assign-assignment)。バッファが「:」として定義されている場合、アルゴリズムは単に新しいトークンを追加し、次の記号が「=」の場合、1つの割り当て演算子が追加されます。



case States.ASGN:
    if (sm[0] == '=')
    {
        AddBuf(sm[0]);
        AddLex(Lexemes, 2, 4, buf);
        ClearBuf();
        GetNext();
    }
    else
        AddLex(Lexemes, 2, 3, buf);
    state = States.S;
break;


最終状態とエラーのある状態は、サービスメッセージによってのみ実装されます。このオプションを改善してエラーをチェックすることもできますが、おそらく、この機能はすでにパーサーに転送されている可能性があります。



case States.ER:
    MessageBox.Show("  ");
    state = States.FIN;
    break;
case States.FIN:
    MessageBox.Show("  ");
    break;


テスト



アルゴリズムはさまざまな方法でテストできます。.pasファイルのパスを直接指定する、プログラムで文字列を作成する、またはその他の便利なオプションです。C#で記述しているため、アプリケーションにフォームを追加することは難しくありません。このフォームには2つのtextBoxが含まれ、1つ目はプログラムコードを入力し、2つ目はアルゴリズムの結果を表示します。



ボタンを押すと、テキスト分析が開始され、結果の結果がスイッチ構造を使用して処理されます。さらに、見つかったトークンのタイプが表示されます。



private void button1_Click(object sender, EventArgs e)
{
    textBox2.Clear();
    TplMain tpl = new TplMain();
    tpl.Analysis(textBox1.Text);
    
    foreach(var lex in tpl.Lexemes)
    {
        switch (lex.id)
        {
            case 1:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "   "+ Environment.NewLine;
                break;
            case 2:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "  " + Environment.NewLine;
                break;
            case 3:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "  " + Environment.NewLine;
                break;
            case 4:
                textBox2.Text += "id: " + lex.id + " lex: " + lex.lex + " val: " + lex.val + " |" + "  " + Environment.NewLine;
                break;
                
        }     
    }       
}


入力データ



program hellohabr;
	var a, b, c : integer;
begin
	c := a - b + 15;
end.


出力



id: 1 lex: 0 val: program |   
id: 4 lex: 1 val: hellohabr |  
id: 2 lex: 1 val: ; |  
id: 1 lex: 1 val: var |   
id: 4 lex: 1 val: a |  
id: 2 lex: 2 val: , |  
id: 4 lex: 1 val: b |  
id: 2 lex: 2 val: , |  
id: 4 lex: 1 val: c |  
id: 2 lex: 3 val: : |  
id: 1 lex: 2 val: integer |   
id: 2 lex: 1 val: ; |  
id: 1 lex: 5 val: begin |   
id: 4 lex: 1 val: c |  
id: 2 lex: 4 val: := |  
id: 4 lex: 1 val: a |  
id: 2 lex: 6 val: - |  
id: 4 lex: 1 val: b |  
id: 2 lex: 5 val: + |  
id: 3 lex: 1 val: 15 |  
id: 2 lex: 1 val: ; |  
id: 1 lex: 6 val: end |   
id: 2 lex: 0 val: . |  


完全なアルゴリズム



public void Analysis(string text)
{
    sr = new StringReader(text);
    while (state != States.FIN)
    {
        switch (state)
        {

            case States.S:
                if (sm[0] == ' ' || sm[0] == '\n' || sm[0] == '\t' || sm[0] == '\0' || sm[0] == '\r')
                    GetNext();
                else if (Char.IsLetter(sm[0]))
                {
                    ClearBuf();
                    AddBuf(sm[0]);
                    state = States.ID;
                    GetNext();
                }
                else if (char.IsDigit(sm[0]))
                {
                    dt = (int)(sm[0] - '0');
                    GetNext();
                    state = States.NUM;

                }
                else if (sm[0] == '{')
                {
                    state = States.COM;
                    GetNext();
                }
                else if (sm[0] == ':')
                {
                    state = States.ASGN;
                    ClearBuf();
                    AddBuf(sm[0]);
                    GetNext();
                }
                else if (sm[0] == '.')
                {
                    AddLex(Lexemes, 2, 0, sm[0].ToString());
                    state = States.FIN;
                }
                else
                {
                    state = States.DLM;

                }

                break;
            case States.ID:
                if (Char.IsLetterOrDigit(sm[0]))
                {
                    AddBuf(sm[0]);
                    GetNext();
                }
                else
                {
                    var srch = SerchLex(Words);
                    if (srch.Item1 != -1)
                        AddLex(Lexemes, 1, srch.Item1, srch.Item2);
                    else
                    {
                        var j = PushLex(TID, buf);
                        AddLex(Lexemes, 4, j.Item1, j.Item2);
                    }
                    state = States.S;
                }
                break;

            case States.NUM:
                if (Char.IsDigit(sm[0]))
                {
                    dt = dt * 10 + (int)(sm[0] - '0');
                    GetNext();
                }
                else
                {

                    var j = PushLex(TNUM, dt.ToString());
                    AddLex(Lexemes, 3, j.Item1, j.Item2);
                    state = States.S;
                }
                break;
            case States.DLM:
                ClearBuf();
                AddBuf(sm[0]);

                var r = SerchLex(Delimiter);
                if (r.Item1 != -1)
                {
                    AddLex(Lexemes, 2, r.Item1, r.Item2);
                    state = States.S;
                    GetNext();
                }
                else
                    state = States.ER;
                break;
            case States.ASGN:
                if (sm[0] == '=')
                {
                    AddBuf(sm[0]);
                    AddLex(Lexemes, 2, 4, buf);
                    ClearBuf();
                    GetNext();
                }
                else
                    AddLex(Lexemes, 2, 3, buf);
                state = States.S;

                break;
            case States.ER:
                MessageBox.Show("  ");
                state = States.FIN;
                break;
            case States.FIN:
                MessageBox.Show("  ");
                break;
        }
    }
}


結論



字句アナライザーはあまり明確ではなく、実際にはそれほど重要ではないように思われるかもしれません。なぜこれらすべてをパーサーに入れることができないのですか?複雑な構造を扱う方法は?はい、字句アナライザーを実装する方法はコンパイラーごとに異なりますが、これらすべての原則を分析すると、Xプログラミング言語がどのように機能するかを理解するだけでなく、独自のプログラミング言語(2番目のPython、またはドメイン用の言語)を開発するための基盤も得られます。仕事のすべての詳細と一般的なコンパイラのデバイスを理解して実現する。



プロジェクトはここで見つけることができます



All Articles