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