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


VB.NETで計算機を作る(乗算・除算)
(準備中)


前回(「VB.Netで計算機を作る(足し算引き算)」)では、再帰下降パースによる足し算引き算の実装をして
みましたが、今回は乗算(掛け算)・除算(割り算)と括弧( )を使えるようにしてみます。


Cによる完成形のプログラムは「プログラミング言語を作る」のP57〜P70にあります。
本に掲載されているプログラムは、再帰下降パーサによる完成されたプログラムで、
字句解析ー>構文解析ー>演算まで2段抜かしでやってしまうようなプログラムです。

このページでは、各プロセスの動きを1段づつ確認したいので、本にあるような洗練されたものとはかなり違う
無骨なプログラムを作っていきます。各プロセスのアルゴリズム、動作の確認を主眼においているので、
正しくない式が入力された時のチェック(異常系のチェック)は甘くしてます。なので正常系の場合のみちゃんと動作します。


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

プログラミング言語を作る 前橋 和弥
価格:6,560円(税込、送料別) (2024/3/28時点)




「プログラミング言語を作る」(前橋和弥) Amazon



今回は、乗算・除算を処理するための関数として、termという関数を追加し
演算のための基本的な関数はexpression, term, primary_expression, get_tokenの4つ

括弧( )の処理は、primary_expressionの中で行い、expressionを再帰的に呼び出すことで
実装可能です。( )の処理がこのように実装できてしまうところが再帰下降パーサによる構文解析の
いいところでもあります。

BNF記法で定義すると

expression: term | expression + term | expression - term;
term: primary_expression | term * primary_expression | term / primary_expression;
primary _expression: number; | (expression)


構文図式で表すのが一番わかりやすいと思いますが、以下の通りです。
expressionは演算記号"+","-"以外が来たときはループを抜け値をリターンします。
termは演算記号"*","/"以外が来たときはループを抜け値をリターンします。
expressionもtermもその場合、自分が処理すべき以外の記号はいったん式に戻し(unget_token)
それからループを抜けます。
例えば、termは "+","-",")"が来るとwhileループを抜け、expressionに戻ります。
expressionは")"が来るとwhileループを抜け、呼び出したprimary_expressionに戻ります。




前回の(「VB.Netで計算機を作る(足し算引き算)」)に掲載したプログラムからの追加、変更点は
以下の4か所です。。

関数expression でprimary_expressionを呼び出していた部分をtermを呼び出すように変更
関数termの追加。→乗算、除算の処理
関数primary_expressionで、( )の処理を追加
unget_token でtokenのNoを1つ戻す処理を実装


@ expressionの変更点
演算子の左側の数値(v1)と右側の数値(v2)の取得に、関数termを呼び出す。
v1 = term()
v2 = term()


A関数termの追加
基本構造は関数expressionと同じなので、以下のソースコードをご参照下さい。

B関数primary_expressionの変更
括弧( )の処理の追加
左括弧"(" (type.lparen)が来たら、関数expressionを再帰的に呼び出す。
( ) 内の処理が終わるとexpressionからの戻り値をvに保存し、次のtokenで右括弧")" (type.rparen)が来たら
vをreturnする。

 If t.type = type.lparen Then
            v = expression()
            t = get_token()
            If t.type = type.rparen Then Return v Else Console.WriteLine("rparen is missing")
        End If



Cunget_token 今読んでいるtokenの番号をひとつ戻します。

2*(1+-3)の演算をしてみた時の動作をトレースしてみます。
Bで( )の処理に入ります。
DとFのプロセスが少し複雑な動きです。



B以降がやや複雑なので動作を説明します。( )の再帰的な処理と、unget_tokenでtokenを式に戻す
あたりがなかなかわかりにくい個所です。
unget_tokenでtokenを式に戻す動作については、実際は今読んでいるtoken番号をひとつ減らすという
操作で行っています。


Aでは、関数termのなかのv2=primary_expresiionのところまで進んでいます。
B primary_expressionの中で、左括弧"("が登場すると、expressionを再帰的に呼び出します。
C 再帰的に呼び出されたexpressionの中で、1+-3 の処理に入ります。
D termの中で、"+"が出てきたときは、unget_tokenして"+"は式の中に戻します。
  whileループを抜けて、値1をexpressionにリターンします。
  expressionの中であらためて"+"を取り出します。
E expressionの中でv2取得のプロセスに入り、expression -> term ->primary expression -> get_token
  -> primary_expressionで-3を取得 -> termのv1に-3を取得
F termの演算子チェックで右括弧")"に出会う -> unret_tokenで")"を式に戻し、whileループを抜けtermは
  expressionに-3をリターン。
  expressionは 1+-3を計算し、演算結果として-2を得る -> whileループの中でget_tokenで")"を得る。
  ->unget_tokenで")"を式に戻しwhileループを抜ける -> Bで呼び出された ( )の結果として-2をリターン
  -> ( )の処理を終える。
  primary_expressionはBでexpressionを呼び出した次のコードの実行に移る。-> get_tokenで")"を取得したので、-2を
  リターン  -> termに戻りtermののv2=-2が取得される。
G termでv1*v2の計算が行われ、結果として-4が得られる。 ->whileループのget_tokenで"line_end"を取得
  ->whileループを抜け、-4をexpressionにリターン

@〜Gの動作フローを図示してえみます。各関数が自分の受け持った処理だけを行い、他の処理は他の関数
に丸投げしていますが、非常に巧妙に動作していることがわかると思います。


( )が複数重なってもちゃんと処理できます。
ちなみに演算用の変数v1,v2はdouble型ですので演算結果は小数点が出てきても大丈夫です。
2*(5/(1+2))+2 を演算させてみた結果
(途中はいくらか省略しています)



ソースコード(変更、追加のあった個所のみ)


    Function expression() As Double
        Dim v1, v2 As Double
        Dim t As token

        v1 = term()
        Console.WriteLine("expression v1:{0}", v1)
        While (1)
            t = get_token()
            Console.WriteLine("expression {0}: {1} : {2} ", t.str, t.type, t.v)
            If (t.type <> type.add_op And t.type <> type.sub_op) Then unget_token() : Exit While
            v2 = term()
            Console.WriteLine("expression v2:{0}", v2)
            If t.type = type.add_op Then v1 = v1 + v2
            If t.type = type.sub_op Then v1 = v1 - v2
            Console.WriteLine("expression calculated  v1:{0}", v1)
        End While

        Return v1

    End Function

    Function term() As Double
        Dim v1, v2 As Double
        Dim t As token

        v1 = primary_expression()
        Console.WriteLine("term v1:{0}", v1)
        While (1)
            t = get_token()
            Console.WriteLine("term {0}: {1} : {2} ", t.str, t.type, t.v)
            If (t.type <> type.mul_op And t.type <> type.div_op) Then unget_token() : Exit While
            v2 = primary_expression()
            Console.WriteLine("term v2:{0}", v2)
            If t.type = type.mul_op Then v1 = v1 * v2
            If t.type = type.div_op Then v1 = v1 / v2
            Console.WriteLine("term calculated  v1:{0}", v1)
        End While

        Return v1
    End Function


    Function primary_expression() As Double
        Dim t As token
        Dim v As Double
        t = get_token()
        Console.WriteLine("primary_expression {0}: {1} : {2} ", t.str, t.type, t.v)
        If t.type = type.number Then Return t.v
        If t.type = type.lparen Then
            v = expression()
            t = get_token()
            If t.type = type.rparen Then Return v Else Console.WriteLine("rparen is missing")
        End If
    End Function

 Function get_token() As token
        Static Dim i As Integer = 0
        REM       t = tok(i)
        If unget = 1 Then i = i - 1 : unget = 0
        Console.WriteLine("get_token {0}: {1} : {2} :{3} ", i, tok(i).str, tok(i).type, tok(i).v)
        REM Console.WriteLine("get_token {0}: {1} : {2} :{3} ", num_token, t.str, t.type, t.v)
        i = i + 1
        Return tok(i - 1)
    End Function

    Sub unget_token()
        unget = 1
    End Sub






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