パソコン活用研究5番街(Visual Basic、Excel(VBA)、BASIC プログラミング研究)


VB.NETで計算機を作る(LL解析)

(準備中)


今までは再帰下降パーサによる構文解析で計算機プログラムを作成してきました。
(→ひとまずの完成形はVB.netで計算機を作る(とりあえず完成形2)

再帰下降パーサもLL構文解析の一種ですが、今回はいささか酔狂でLL構文解析表による
計算機プログラムを作成したみます。


実行例




変数
rpn, rpnlist() 
姉妹サイトLL解析表での四則演算にあるrpnlistというのがこのプログラムの変数rpnlに該当し
rpnlの各要素を肘しているのがrpnlist()という配列になります。
rpnpos rpnlistのindex



宣言部と字句解析部
宣言部と字句解析部はVB.netで計算機を作る(とりあえず完成形2)
で作成したプログラムをほぼそのまま流用しています。使用する変数等の違いで多少の違いはありますが。

@のForループでは、入力文字列(記号列)を1文字づつ表示
statement.Length
文字列statementの長さを取得するのに、Lengthプロパティを使っている。 

AのForループでは、入力文字列をに分解しtokenに分解したものを表示

Imports System

Module Program
    REM    Public num_token As Integer
    Public err, rpnpos, rpnstackpos As Integer
    Public rpnlist(30), rpnstack(30) As String

    Enum type
        number
        dot
        add_op
        sub_op
        mul_op
        div_op
        pow_op
        lparen
        rparen
        space
        bad
        line_end

    End Enum

    Structure token
        Dim type As type
        Dim str As String
        Dim v As Double
    End Structure

    Public tok(20) As token

    Sub Main(args As String())
        Dim i, j, k, l, pos, isnum, rp, lp As Integer
        Dim value, v As Double
        Dim r As type
        Dim statement, stack, c, rpnl, rpnstck, calclist(20) As String

        REM     num_token = 0
        err = 0 : rp = 0 : lp = 0

        statement = Console.ReadLine()

' @ 入力文字列(記号列)を1文字づつ表示
        For i = 0 To statement.Length - 1
            Console.WriteLine("{0} : {1}", i, statement.Chars(i))
        Next

        pos = 0 : isnum = 0
        For i = 0 To statement.Length - 1
            Try
                r = checktype(statement.Chars(i))
                If (isnum = 0 Or isnum = 5) And r = type.space Then GoTo L1 : 
                If (isnum = 1 Or isnum = 3) And r = type.space Then isnum = 5 : GoTo L1 : 
                If r = type.bad Then Console.WriteLine("{0}:Bad character", i) : tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.bad : err = 1 : Exit For
                If (isnum = 1 Or isnum = 3) And r = type.number Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).v = Double.Parse(tok(pos).str) : GoTo L1
                If isnum = 1 And r = type.dot Then tok(pos).str = tok(pos).str + statement.Chars(i) : isnum = 2 : GoTo L1
                If (isnum = 1 Or isnum = 3 Or isnum = 5) And r = type.rparen Then rp = rp + 1 : pos = pos + 1 : tok(pos).str = statement.Chars(i) : tok(pos).type = r : isnum = 5 : GoTo L1
                If (isnum = 1 Or isnum = 3 Or isnum = 5) And (r = type.add_op Or r = type.sub_op Or r = type.mul_op Or r = type.div_op Or r = type.pow_op) Then pos = pos + 1 : tok(pos).str = statement.Chars(i) : tok(pos).type = r : pos = pos + 1 : tok(pos).type = type.bad : isnum = 0 : GoTo L1
                If isnum = 2 And r = type.number Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).v = Double.Parse(tok(pos).str) : isnum = 3 : GoTo L1
                If isnum = 2 And r <> type.number Then err = 1 : Console.WriteLine("{0}: Bad expression", i) : Exit For
                If isnum = 3 And r = type.dot Then err = 1 : Console.WriteLine("{0}: too many dot", i) : tok(pos).type = type.bad : Exit For
                If isnum = 0 And r = type.lparen Then lp = lp + 1 : tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = r : pos = pos + 1 : GoTo L1
                If isnum = 0 And r = type.number Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.number : tok(pos).v = Integer.Parse(tok(pos).str) : isnum = 1 : GoTo L1
                If isnum = 0 And r = type.sub_op Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.number : isnum = 1 : GoTo L1
                Console.WriteLine("{0}:Bad expression", i) : tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.bad : err = 1 : Exit For
            Catch ex As System.FormatException
                ' (0)0のような表現で")0"がSystem.FormatExceptionを引きおこすが、rparenの処理後isnum=5にすることで解消できる。tok(pos).v = Integer.Parse(tok(pos).str)が")0"という表現を整数型にパースできないため
                err = 1
                Console.WriteLine("System.FormatException")
                Exit For
            End Try
L1:         If lp < rp Then Console.WriteLine("too many rparen") : err = 1 : Exit For
        Next

        If lp <> rp Then Console.WriteLine("The number of parentheses does not match.") : err = 1
        Console.WriteLine("TOKEN")
        tok(pos + 1).type = type.line_end
        tok(pos + 1).str = "$"
'  A tokenの表示
        For i = 0 To pos + 1
           Console.WriteLine("{0}: {1} : {2} : {3}", i, tok(i).str, tok(i).type, tok(i).v)
        Next

        If err = 1 Then Console.WriteLine("This is a bad expression")




構文解析
以下構文解析を行っている個所になります。
stackという変数は、姉妹サイト「LL解析表での四則演算」のLL解析表にあるスタックに該当します。
解析の1ステップ毎に
B rpnl(姉妹サイト「LL解析表での四則演算」のLL解析表にあるrpnlist)の確定と作成
C rpnstck(姉妹サイト「LL解析表での四則演算」のLL解析表にあるrpnstack)の確定と作成
DよりLL解析表に基づく解析を行っています。
D-A
stackより先頭の記号を読み込み、その記号が"$"かつ現在解析中のtoken=記号列 (tok(i))のtypeがline_endなら解析終了
D-B
stackの先頭の記号が"n"かつ現在解析中のtoken=記号列 (tok(i))のtypeがnumber(数値)ならstackの先頭から1文字ポップし、rpn()を呼ぶ。
また、stackの先頭記号と現在解析中のtokenの記号が同じ場合もstackの先頭から1文字ポップし、rpn()を呼ぶ
stack.Remove(0,1)とありますが、Removeというメソッドは文字列からRemoveで指定した文字を削除するメソッドです。
Remove(0,1)は先頭の文字の削除になります。
Remove(1,3)だと2文字目から3文字削除になります。
D-C以降、Select Case文の中で、stackの先頭の記号が"E","A","T","B","F"の場合に分けて処理を進めます。
姉妹サイト「LL解析表での四則演算」のLL解析表(表1)に基づいた処理です。

E D-Aで構文解析が終了したら、後置記法を完成させるために、rpnstackに残っている記号(文字)を取り出し、rpnlistに追加していきます。
これで後置記法(rpnl, rpnlist() )が完成です。

F後置記法に変換した後の式の表示です。

  stack = "E$"
        Console.WriteLine("Analisys")
        i = 0 : j = 0 : rpnlist(0) = ""
        While (1)
            rpnl = "" : rpnstck = ""
' Brpnl(rpnlist)
            For j = 0 To rpnpos
                rpnl = rpnl + rpnlist(j) + " "
            Next
' Crpnstck(rpnstack)
            For j = 0 To rpnstackpos
                rpnstck = rpnstck + rpnstack(j) + " "
            Next
' D
            Console.WriteLine("{0}: {1}: {2}: {3}", tok(i).str, stack, rpnl, rpnstck)

            c = stack.Chars(0)
            If (c = "$" And tok(i).type = type.line_end) Then Exit While   'D-A
            If (c = "n" And tok(i).type = type.number) Or (c = tok(i).str) Then stack = stack.Remove(0, 1) : rpn(i) : i += 1 : GoTo L2   'D-B
'D-C
            Select Case c
                Case "E"
                    If (tok(i).type = type.number) Or (tok(i).type = type.lparen) Then stack = "TA" + stack.Remove(0, 1) Else err = err + 10 : Exit While
                Case "A"
                    If (tok(i).type = type.add_op) Then stack = "+TA" + stack.Remove(0, 1) : GoTo L2
                    If (tok(i).type = type.sub_op) Then stack = "-TA" + stack.Remove(0, 1) : GoTo L2
                    If (tok(i).type = type.rparen) Or (tok(i).type = type.line_end) Then stack = stack.Remove(0, 1) : GoTo L2
                    err = err + 10 : Exit While
                Case "T"
                    If (tok(i).type = type.number) Or (tok(i).type = type.lparen) Then stack = "FB" + stack.Remove(0, 1) Else err = err + 10 : Exit While
                Case "B"
                    If (tok(i).type = type.add_op) Or (tok(i).type = type.sub_op) Then stack = stack.Remove(0, 1) : GoTo L2
                    If (tok(i).type = type.mul_op) Then stack = "*FB" + stack.Remove(0, 1) : GoTo L2
                    If (tok(i).type = type.div_op) Then stack = "/FB" + stack.Remove(0, 1) : GoTo L2
                    If (tok(i).type = type.rparen) Or (tok(i).type = type.line_end) Then stack = stack.Remove(0, 1) : GoTo L2
                    err = err + 10 : Exit While
                Case "F"
                    If (tok(i).type = type.number) Then stack = "n" + stack.Remove(0, 1) : GoTo L2
                    If (tok(i).type = type.lparen) Then stack = "(E)" + stack.Remove(0, 1) : GoTo L2
                    err = err + 10 : Exit While
            End Select


L2:
        End While

' E後置記法を完成させるための最後の処理
       For i = rpnstackpos To 0 Step -1
            rpnlist(rpnpos + 1) = rpnstack(i)
            rpnl = rpnl + rpnlist(rpnpos + 1) + " "
            rpnpos = rpnpos + 1
        Next

' F 後置記法に変換後の表示
        For k = 0 To rpnpos
            Console.WriteLine(rpnlist(k))
        Next
        Console.WriteLine(rpnl)


main()の後半

main()の後半は実際の演算部が中心です。

Gからの3行はエラーがあった場合の処理です。
エラーがあった場合は演算を実施せず終了します。
Hからが演算部です。

 ' Gエラーがある場合は演算を実施せず終了
        If (err Mod 10) = 1 Then Console.WriteLine("Error on tokenization") : GoTo L3
        If err >= 10 Then Console.WriteLine("Error on calculation") : GoTo L3
        If lp <> rp Then Console.WriteLine("The number of parentheses does not match.") : GoTo L3

' H
        Console.WriteLine("Caluculation")

        k = 0 : l = 0

        For i = 0 To rpnpos
            If rpnlist(i) = "+" Then
                v = Double.Parse(calclist(k - 2)) + Double.Parse(calclist(k - 1))
                calclist(k - 2) = v.ToString() : k = k - 1
                Console.WriteLine(v)
            End If

            If rpnlist(i) = "-" Then
                v = Double.Parse(calclist(k - 2)) - Double.Parse(calclist(k - 1))
                calclist(k - 2) = v.ToString()
                k = k - 1
                Console.WriteLine(v)
            End If


            If rpnlist(i) = "*" Then
                v = Double.Parse(calclist(k - 2)) * Double.Parse(calclist(k - 1))
                calclist(k - 2) = v.ToString()
                k = k - 1
                Console.WriteLine(v)
            End If

            If rpnlist(i) = "/" Then
                v = Double.Parse(calclist(k - 2)) / Double.Parse(calclist(k - 1))
                calclist(k - 2) = v.ToString()
                k = k - 1
                Console.WriteLine(v)
            End If

            If rpnlist(i) <> "+" And rpnlist(i) <> "-" And rpnlist(i) <> "*" And rpnlist(i) <> "/" Then calclist(k) = rpnlist(i) : k = k + 1

            REM           For l = 0 To k
            REM           Console.Write("{0}:", calclist(l))

            REM        Next
            REM    Console.WriteLine()

        Next

        value = v

        Console.WriteLine("value: {0}", value)

L3:

    End Sub


後置記法への変換
プロシージャrpn()は、構文解析中(D-B)に呼び出され、後置記法への変換の処理を行う。


Sub rpn()
変数jはrpnlistのカウント用、kはrepstackのカウント用です。
I tokenのtypeが数値だった場合は、rpnlist()に入れる
J tokenのtypeが"("左括弧だった場合は、rpnstack()に入れる

以下、tokenが"+",-","*","/",")"だった場合の処理は「LL解析表での四則演算」の後置記法への置き換えルール
に記した処理をそのままコードにしています。

最後にrpnposの値とrpnstackposの値を設定したいます。

  Sub rpn(ByVal i As Integer)
        Static j As Integer = 0
        Static k As Integer = 0
        Dim l As Integer
        Console.WriteLine(tok(i).str)
        If (tok(i).type = type.number) Then rpnlist(j) = tok(i).str : j = j + 1  'I
        If (tok(i).type = type.lparen) Then rpnstack(k) = tok(i).str : k = k + 1  'J

        If (tok(i).type = type.add_op) Or (tok(i).type = type.sub_op) Then

            While (1)
                If k = 0 Then Exit While
                If rpnstack(k - 1) = "(" Then Exit While
                If (rpnstack(k - 1) = "+") Or (rpnstack(k - 1) = "-") Or (rpnstack(k - 1) = "*") Or (rpnstack(k - 1) = "/") Then rpnlist(j) = rpnstack(k - 1) : j = j + 1 : rpnstack(k - 1) = "" : k = k - 1

            End While
            rpnstack(k) = tok(i).str : k = k + 1
        End If

        If (tok(i).type = type.mul_op) Or (tok(i).type = type.div_op) Then

            While (1)
                If k = 0 Then Exit While
                If rpnstack(k - 1) = "(" Or rpnstack(k - 1) = "+" Or rpnstack(k - 1) = "-" Then Exit While
                If rpnstack(k - 1) = "*" Or rpnstack(k - 1) = "/" Then rpnlist(j) = rpnstack(k - 1) : j = j + 1 : rpnstack(k - 1) = "" : k = k - 1

            End While
            rpnstack(k) = tok(i).str : k = k + 1
        End If

        If tok(i).type = type.rparen Then

            While (1)
                If k = 0 Then err = err + 10 : Exit While
                If rpnstack(k - 1) = "(" Then rpnstack(k - 1) = "" : k = k - 1 : Exit While
                If rpnstack(k - 1) = "+" Or rpnstack(k - 1) = "-" Or rpnstack(k - 1) = "*" Or rpnstack(k - 1) = "/" Then rpnlist(j) = rpnstack(k - 1) : j = j + 1 : rpnstack(k - 1) = "" : k = k - 1
            End While


        End If
        rpnpos = j - 1 : rpnstackpos = k - 1
        REM        Console.WriteLine("{0}, {1}", rpnpos, rpnstackpos)
    End Sub


字句解析のための関数checktype() とEnd Module
ここはVB.netで計算機を作る(とりあえず完成形2)
のものをそのまま流用しています。

最後の1行はこのモジュールの終了をあらわすEnd Module

  
    Function checktype(ByVal t As String) As Integer
        If IsNumeric(t) = True Then Return type.number
        If t = "." Then Return type.dot
        If t = "+" Then Return type.add_op
        If t = "-" Then Return type.sub_op
        If t = "*" Then Return type.mul_op
        If t = "/" Then Return type.div_op
        If t = "^" Then Return type.pow_op
        If t = "(" Then Return type.lparen
        If t = ")" Then Return type.rparen
        If t = " " Then Return type.space
        Return type.bad

    End Function


End Module







TopPage > Visual BasIc&Excel活用研究目次