MikoScript 言語仕様

 少し高度なプログラムの紹介

 ここでは、本言語のエッセンス(真髄)の幾つかを紹介します。そのために、少し高度
なプログラムの例を示します。ちなみに、これに相当するものは他の言語でも記述できる
場合がありますが、本言語では、比較的簡潔に記述でき、追加のライブラリーも不要です。

●数式解析プログラム
 これは、数式を表わす文字列を解析して、その計算結果をプリントするプログラムです。
この数式には、四則演算と括弧が使えます。例えば、"1+2*3" という文字列に対しては、
7 という結果をプリントします。また、"(4+5)*(6-(7-8))/90" では、0.7 になります。
さらに、この数式では、コンマ等で区切った複数の式を記述でき、変数も使えます。
例えば、"A=1, B=2, C=A+B" では、1, 2, 3 の計算結果をプリントします。

 このようなプログラムを実現する方法としては、再帰降下法がよく知られていますが、
ここでは、本言語のエッセンスがもっと顕著に表れる方法を示します。この方法では、
まず、数式の文字列を解析して、それに対応する数式木を構築します。そして、その木
構造に沿って演算を行なっていきます。

 数式の解析では、まず、その文字列から字句要素を抽出する必要があります。数式の
字句要素としては、数値、変数、各種演算子、左右の丸括弧、式の区切り、空白などが
あります。また、数式木を構成するノードの種類としては、根元、数値、変数、演算子
が必要です。これらの識別値をここでは、次のようにマクロで定義しています。なお、
マクロ定義は、C 言語では、#define を使いますが、本言語では、#set を使います。
// 字句要素のトークン値とノードの種別(兼用)
#set    TV_ROOT         0       // 根元節
#set    TV_NUMBER       1       // 数値
#set    TV_NAME         2       // 変数名
#set    TV_OP           3       // 演算子    + - * / =
#set    TV_LP           4       // 左丸括弧  (
#set    TV_RP           5       // 右丸括弧  )
#set    TV_DELIM        6       // 数式の区切り
#set    TV_SPACE        7       // 空白
#set    TV_BADCHAR      8       // 不正文字
 数式木というのは、根元節から始まり、演算子を分枝節、定数項と変数項を終端節と
して木構造に表わしたものです。たとえば、「A+B」の数式は、次のような数式木に
なります。なお、以下で、R は Root の意で根元節を示します。
          ┌─ A
    R ─(+)─┤
          └─ B
「A−B*C」の数式は、次の数式木になります。
          ┌─ A
    R ─(−)─┤
         │     ┌─ B
          └─(*)─┤
               └─ C
「(X+1)/(Y−2)」の数式は、次の数式木になります。
               ┌─ X
          ┌─(+)─┤
         │     └─ 1
    R ─(/)─┤
         │     ┌─ Y
          └─(−)─┤
               └─ 2
 数式木を構成する根元節、演算子の分枝節、定数項と変数項の終端節を総称して、
ノード(または節)と呼びます。このノードは、クラスとして、次のように定義して
います。
class   ^NODE   // 数式木を構成するノード・クラス
{
    .PRIOR ::=  // 評価の優先度(値が大きいほど高い)
    {
        .[ #TV_ROOT ]    = 0;   // 根元ノード
        .[ #TV_OP, "=" ] = 1;   // 代入演算子ノード
        .[ #TV_OP, "+" ] = 2;   // 加算演算子ノード
        .[ #TV_OP, "-" ] = 2;   // 減算演算子ノード
        .[ #TV_OP, "*" ] = 3;   // 乗算演算子ノード
        .[ #TV_OP, "/" ] = 3;   // 除算演算子ノード
        .[ #TV_NUMBER ]  = 4;   // 数値ノード
        .[ #TV_NAME ]    = 4;   // 変数ノード
    }

    .LINKS ::=  // 接続可能な下位ノードの最大個数
    {
        .[ #TV_ROOT ]    = 1;   // 根元ノード
        .[ #TV_OP, "=" ] = 2;   // 代入演算子ノード
        .[ #TV_OP, "+" ] = 2;   // 加算演算子ノード
        .[ #TV_OP, "-" ] = 2;   // 減算演算子ノード
        .[ #TV_OP, "*" ] = 2;   // 乗算演算子ノード
        .[ #TV_OP, "/" ] = 2;   // 除算演算子ノード
        .[ #TV_NUMBER ]  = 0;   // 数値ノード
        .[ #TV_NAME ]    = 0;   // 変数ノード
    }

    function Construct( type, value, rank )   // コンストラクタ
    {
        .Type = type;   // 種類
        .Val = value;   // 値(演算子の場合はその機能を示す値)
        .Weight =       // 重さ(重いノードほど下位になるように数式木を形成)
            ( type == #TV_OP ? .PRIOR[ #TV_OP, value ] : .PRIOR[ type ] )
            + ( rank << 4 );
        .Links =        // 下位ノードの最大個数(演算子の場合は項数)
            ( type == #TV_OP ? .LINKS[ #TV_OP, value ] : .LINKS[ type ] );
    }

    //function Destruct()  {}   // デストラクタ(このクラスでは不要)
}
 本言語では、クラスは、単なる雛型ではなく、オブジェクトとして存在します。ここで、
NODE クラスは、^ 演算子が付いているので、モジュールローカルスコープのオブジェクト
として生成されます。ちなみに、:: 演算子が付いていれば、グローバルスコープに、何も
付いていなければ、関数のローカルスコープに生成されます。

 この NODE クラスには、.PRIOR と .LINKS というメンバー変数が設定されていますが、
これらは、NODE クラスの各インスタンスごとに割り当てられるものではなく、あくまで、
共通に参照されるものです。ちなみにこれらは、C++ 言語のクラスの static メンバーに
相当します。

 .PRIOR は、各ノードの種類に対応する評価の優先度を示す表になります。例えば、+ 
演算子の評価の優先度は、2 で、* 演算子は、3 になります。これは、* の方が、+ より
優先的に評価されることに対応しています。つまり、1+2*3 は、(1+2)*3 して評価される
のではなく、1+(2*3) として評価されることに対応しています。

 .LINKS は、各ノードの種類に対応して、その下位ノードを何個まで接続可能かを示す
表になります。例えば、四則演算子は、2項演算子なので、下位ノードは2個まで接続
可能です。数値ノード(定数項)は、終端ノードなので、その下位ノードはありません。

 コンストラクタは、インスタンスの生成の際に自動的に呼ばれる特別な関数で、通常、
そのインスタンスの初期設定を行ないます。NODE クラスのコンストラクタは、.Type, 
.Val, .Weight, .Links のメンバー変数を設定しています。これらは、各インスタンス
固有のプロパティーになります。

 さて、以下に示す Parse() という関数は、引数で渡された数式の文字列を解析して、
その計算結果をプリントします。例えば、
      Parse( "12+34" );
を実行すると、46 という計算結果をプリントします。また、
      Parse( "x = 5, y = -6.7, - ( -x + 8.9 ) * 10 - ( 1.1 + y )" );
を実行すると、次のような結果をプリントします。
   5
   -6.7
   -33.4
function ^Parse( text )  // 数式の文字列を解析して計算結果をプリントする
{
    Text = ::Buffer();      // 数式の文字列を格納するバッファを生成
    Text.Write( text );     // このバッファに数式の文字列を書き込む
    Text.Seek( 0 );         // 現在位置をバッファの先頭に移動

    Text.Find(  // 数式の字句解析用正規表現で検索開始
        $"\d+(\.\d*)?#1|"   // 数値                 #TV_NUMBER
        $"\a\w*#2|"         // 変数名               #TV_NAME
        $"[+\-*/=]#3|"      // 演算子               #TV_OP
        $"\(#4|"            // 開括弧               #TV_LP
        $"\)#5|"            // 閉括弧               #TV_RP
        $"[,;\n]#6|"        // 数式の区切り         #TV_DELIM
        $"[ \t]+#7|"        // 空白                 #TV_SPACE
        $".#8"              // その他(不正文字)   #TV_BADCHAR
    );

    for( ;; )   // 各数式の区切りごとに処理する
    {
        call MakeTree;      // 数式木を形成
        EvalTree( Root );   // 数式木を評価(計算結果をプリント)
        if( END? )          // 終了フラグセット?
            break;
        //delete Root;  // ← この実行がなくても数式木は自動的に破棄される
    }
    return #OK;

  MakeTree:
    Root = ^NODE( #TV_ROOT, null, rank = 0 );   // 根元ノードを生成
    np := Root;     // 現参照ノード(np)を根元ノードに設定
    do    // 各字句要素を検索して数式木を形成していく
    {
        switch( tv = Text.FoundID() )  // 字句要素のトークン値で分岐
        {
          case #TV_NUMBER:  Node = ^NODE( tv, Text.FoundText()'float, rank );
                            break;
          case #TV_NAME:    Node = ^NODE( tv, Text.FoundText(), rank );
                            break;
          case #TV_OP:      Node = ^NODE( tv, Text.FoundText(), rank );
                            break;
          case #TV_LP:      rank++;   // 括弧の深さごとに重さのランクを上げる
                            continue;
          case #TV_RP:      if( --rank < 0 )    // 対応する右括弧がない?
                                warp SyntaxError;
                            continue;
          case #TV_DELIM:   if( rank > 0 )      // 括弧が閉じていない?
                                warp SyntaxError;
                            Text.FindNext();    // 次の字句要素の事前検索
                            back;               // call 元に戻る
          case #TV_BADCHAR: warp BadChar;
          case #TV_SPACE:   continue;
          default:          quit;   // 最後まで検索し終えたらループを抜ける
        }

        // 現参照ノードを新ノードよりも軽いノードへ移動
        while( Node.Weight <= np.Weight )
            np := np'up;

        if( np[1]'exist? )      // 現参照ノードの右項は接続済?
        {
            if( Node.Links < 2 )    // 新ノードは二項演算子ではない?
                warp SyntaxError;
            Node[0] <- np[1];   // 新ノードの左項へ現参照ノードの右項を移動代入
        }
        else    // 現参照ノードの右項が空きの場合
        if( np.Links >= 2  &&   // 現参照ノードは二項演算子?
            ! np[0]'exist? )    // 現参照ノードの左項も空き?
        {
            if( np.Val != "-"  &&  np.Val != "+" )  // 符号用の演算子ではない?
                warp SyntaxError;
            np.Weight += 2;     // 数値ノードと同じ重さにする
        }
        np[1] <- Node;  // 現参照ノードの右項へ新ノードを移動代入
        np := np[1];    // 現参照ノードを新ノードにする
    }
    while( Text.FindNext() > 0 );   // 次の字句要素を検索

    END? = #TRUE;   // 終了フラグをセット
    back;           // call 元に戻る

  SyntaxError:
    print "文法エラー!";
    goto ERR_FIN;
  BadChar:
    print "不正文字: ": Text.FoundText()'quote;
    goto ERR_FIN;
  NoSuchVar:
    print "変数なし: ": ^Name;
    goto ERR_FIN;
  ZeroDiv:
    print "0除算!";
  ERR_FIN:
    $err_state = 0;
    return #NG;
}
 数式の文字列の解析には、その字句解析用の正規表現を使います。そのために、まず、
::Buffer クラスのインスタンスを生成して、そこに数式の文字列を格納しておきます。
そして、各字句要素を正規表現で検索していきます。この正規表現は、最初に検索を開始
する時にだけ指定し、2回目以降の検索では指定する必要がありません。

 数式は、コンマ、セミコロン、または、改行で、複数の式に区切れます。その各区切り
ごとに、数式木を形成して評価していきます。数式木を形成するのは、MakeTree という
ラベル以降にあるサブルーチンで、数式木を評価するのは、EvalTree() という関数です。

 サブルーチンは、call文で呼び出します。サブルーチンの呼び出しは、関数の呼び出し
よりも遥かに高速です。また、サブルーチンでは、呼び出し元と呼び出し先でのローカル
スコープが同じなので、特に引数を指定する必要はありません。サブルーチンは、back文
で、呼び出し元に戻ります。

 MakeTree のサブルーチンでは、まず、根元ノードを生成します。そして、その下に、
各種のノードを順次生成して連結していきます。

 各ノードは、正規表現で検索した字句要素の識別値に応じて、その種類を決めます。
各識別値は、前述のようにマクロ定義されているので、それを使う時は、そのマクロ名
の前に # を付けます。

 NODE クラスのインスタンスの生成は、その NODE というクラス名の関数をコールする
のと同様の形態で、その引数には、コンストラクタの引数を指定します。この形態では、
C++ 言語のような new は不要です。

 本言語では、木構造は、非常に容易に形成できます。また、既に形成されている木構造
内の部分木の移動も非常に簡単です。例えば、あるノードの下に新ノードを追加する場合、
そのノードのインスタンスを普通に代入するだけです。ちなみに、2項演算子のノードは、
[0] と [1] というインデックスに、その左項と右項のノードをそれぞれ代入しています。
また、部分木の移動は、<- という演算子を使って、移動代入で行なっています。

 各ノードは、基本的に、重いものが軽いものよりも下位になるように連結していきます。
各ノードの重さ( .Weight )は、その評価の優先度( .PRIOR[] )と括弧の深さ( rank )に
依存します。また、各ノードでは、その下位に接続可能なノードの最大個数( .LINKS[] )
が決まっているので、過不足がないように連結していきます。

 数式の文字列が、想定通りの文法になっていない場合、所定のエラー処理部にワープし
ます。ワープは、warp文で書きます。これは、goto文に似ていますが、ワープでは、現在
実行中の関数内だけでなく、その呼び出し元の関数を順に辿ってその中にあるラベルへも
ジャンプすることができます。

 上記関数内には、'float, 'up, 'exist?, 'quote のような表記がありますが、これらは、
リレー型関数(後置型関数)で、本言語システムにあらかじめ組み込まれているものです。
リレー型関数のコール形態は、本言語特有で、例えば、x'func は、標準の関数コール形態
では、func( x ) となります。これは、単なる表記上の違いだけでなく、長所もいろいろと
あります。

 さて、以下に示す EvalTree() という関数は、上記の MakeTree で形成された数式木を
引数で受け取ります。そして、それを評価して計算結果をプリントします。この関数は、
木構造内の各ノードを深さ優先でたどっていきます。その際、再帰呼び出し(自分で自分
を呼び出す)も行なっています。
function EvalTree( np )   // 数式木を評価して計算結果をプリントする
{
    if( np[0]'exist?  &&  np[0].Links > 0 )   // 左項は分岐ノード?
        EvalTree( np[0] );   // 左項の評価へ(再帰呼出)

    if( np[1]'exist?  &&  np[1].Links > 0 )   // 右項は分岐ノード?
        EvalTree( np[1] );   // 右項の評価へ(再帰呼出)

    if( np[0].Type == #TV_NAME  &&  // 左項は変数名?
        np.Val != "=" )             // 左項は代入先ではない?
    {
        // 左項の変数をその値に変換
        if(( np[0].Val = ^Var[ ^Name = np[0].Val ] ) == ^DefValue )
            warp NoSuchVar;   // その変数が未登録
    }
    if( np[1].Type == #TV_NAME )    // 右項は変数名?
    {
        // 右項の変数をその値に変換
        if(( np[1].Val = ^Var[ ^Name = np[1].Val ] ) == ^DefValue )
            warp NoSuchVar;   // その変数が未登録
    }

    //print np[0].Val, np.Val, np[1].Val;   // @@@ デバッグ用プリント @@@

    switch( np.Val )    // 本ノードの評価(演算)を実行
    {
      case "+":     np.Val = np[0].Val + np[1].Val;     break;
      case "-":     np.Val = np[0].Val - np[1].Val;     break;
      case "*":     np.Val = np[0].Val * np[1].Val;     break;
      case "/":     np.Val = np[0].Val / np[1].Val;     break;
      case "=":     if( np[0].Type != #TV_NAME  || ! np[1]'exist? )
                        warp SyntaxError;
                    np.Val = ^Var[ np[0].Val ] = np[1].Val;
                    break;
      case null:    // 根元ノードの場合(最終演算値をプリント)
                    print np[1].Val;
    }
    return;   // ここで、本ノードの値は確定(評価済)

  // 各演算で例外が発生した時、以下の対応するラベルへエラーワープして来る
  OnOpErr["/"]:     if( np[1].Val == 0 )    warp ZeroDiv;
  OnOpErr["*"]:     warp SyntaxError;

  OnOpErr["-"]:     if( ! np[1]'exist? )    warp SyntaxError;
                    np.Val = - np[1].Val;
                    goto FIN;
  OnOpErr["+"]:     if( ! np[1]'exist? )    warp SyntaxError;
                    np.Val = + np[1].Val;
  FIN:              $err_state = 0;
                    return;
}
 数式木内の各ノードの下位に左項や右項のノードがあるかないかは、'exist? という
リレー型関数で確認できます。

 数式の文字列内に現れる変数は、^Var[] という連想配列で管理しています。この操作
は、非常に簡単で、例えば、abc という変数に、12.3 という値を登録するには、
      ^Var[ "abc" ] = 12.3;
という代入を行なうだけです。また、abc という名前の変数の値を読み出すには、単に、
      ^Var[ "abc" ]
とするだけです。なお、未登録の変数の値を読み出した時は、例外が発生します。例えば、
N という変数が登録されていない時に、
      V = ^Var[ "N" ];
を実行すると、例外が発生します。

 この種の例外の捕捉の仕方にはいろいろありますが、その代替値を使う場合、その値を、
      ^DefValue
という名前の変数に設定しておきます。本プログラムでは、これに、null という無効値
を設定しています。こうしておけば、例えば、上記の V には、null が代入されて処理が
継続します。

 null に対する四則演算や、0 での除算などは、例外を発生します。この種の例外は、
EvalTree() 関数内では、各演算子ごとに捕捉しています。つまり、その例外を起こした
演算子に対応して、
     OnOpErr["演算子"]:
という連想レベルへ、エラーワープするようになっています。その後は、所定のエラー
処理部へさらにワープします。

 さて、^ModuleInit() という名前の関数は、本言語の処理系では、無条件に最初に実行
されることになっています。この関数では通常、各種の初期設定を行ないます。詳細は
「モジュール」の章で説明しています。
function ^ModuleInit()   // モジュール初期設定関数
{
    ^DefValue = null;                   // 例外発生時の代替値を設定
    ^Var[ "pi" ] = 3.1415926535897;     // 円周率 pi の変数を事前登録
}
 本プログラムの ^ModuleInit 関数は、このような処理を行なっているので、不在変数
の代替値が、null になり、円周率を示す pi という変数を数式内で使えるようになって
います。

 ちなみに、^ModuleInit 関数を使わずに、その内容を、プログラムの最初で実行しても
構いません。その方が良い場合もよくあります。しかし、^ModuleInit 関数を使わざるを
得ない場合もあります。ここでは、とりあえず、使ってみています。

 以上で、数式解析プログラムは終りです。この動作を確認するには、例えば、次のよう
にします。
    Parse( "3+4*5" );
    Parse( "x = 1, y = 2, ( x + 3 ) / ( y - 4 ), pi * 10" );
これを実行すると、以下のようにプリントされます。
    23
    1
    2
    -2
    31.4159