使い方:
- source のテキストボックスに、変数定義と式とを入れる。変数定義は","で区切り、変数定義と式とは";"で区切る。
- interpret をクリックすると、interpretation のテキストボックスに計算過程と結果とが表示される。
- redefine のテキストボックスで、変数値を再定義する。
- exec をクリックすると、再定義に基づく計算がなされる。
解説:
コンパイラやインタープリターなど、プログラムを解釈する機能を作りたいとはずっと思っていました。
今回は代入文の右辺などの式を解釈し、数値化するものを作ってみました。
式の解釈は、数式処理などで試みたことがあり、括弧が多重についた式に対処するため、樹構造や、スタックのような構造も試してみました。
しかし、複雑な構造は、見通しが悪く、デバッグがやりづらい点が問題でした。
今回作ってみたものでは、普通の式の順序のまま、各ステップを一行の文字列に変換して見通せます。
配列の要素は文字列と数値のどちらかであり、空白は含みません。
一行の文字列に変換するとき、配列の境目には空白を入れているので、空白があればそれが要素の境目とわかります。
式の解釈(と計算)は、文字列であるもとの式を配列に変換する前処理、配列にした式から1個の数値を求める解釈と(同時の)コンパイル、変数が変更された場合の再計算から成ります。
【式の仕様と前処理】
評価すべき式の仕様として、積についてはプログラミング言語のような書き方ではなく、数学の記法に近いものを許し、"*"、"·" および"×"の他に空白" "が可能です。
例えば、"a b" (間に空白を入れています)はaとbの積として許容します。さらに、"a 2" も積としてOKです。
空白を入れない"ab"や"a2"は積ではなく、それぞれ単一の変数になる一方、数字の次に変数が来る場合に限って、間に空白がなくても積と見做し、"2a" は2とaの積になります。
名前には数字の他に"_"も、また、先頭以外では"."も使えます。
前処理においては、これを名前および数値の配列に作りますが、プログラム実行時に判断し易いように、作業用の中間データでは積には必ず "*" を用いています。
もとの式をこのように改変した配列をlistの初期状態とします。
なお関数はjavascript の Math. で始まる関数をそのまま書くようにしました。Math. の定数も利用できます。
求めるのは式の値ですが、"="を1個含んだ式も可能であり、この場合、 左辺-(右辺) の値が求められます。
これは Tiles の次期 version を狙った仕様です。
【解釈とコンパイル、再計算】
前処理で作られたlistを変形して、答えを作って行きます。
listの上の位置を示すポインタlptrが、現在着目する配列上の位置を示します。
計算は次のループを回すことによって行われます。
- 現在の配列の内容を審査し、次の処理をどうすべきか判断し、「予告」する。この予告は12種類あり、
<→><a→><^><*></><+><0+><-><0-><()><f><.>と名付けられる。
審査されるのは、lptr より前の区間の最後に位置する"("のせいぜい直前から、lptr が指している(すぐ次の)要素までである。
- 有意な変更(定義は後述)があった、または予定される場合には、テキストボックスの内容に現在の状態を追加。
この「現在の状態」とは、まずlistの内容を、要素の間に空白を入れながら表示。
着目するlptrの位置をカーソル"█"で表示。
次にlistの内容を、要素の間に空白を入れながら表示。最後に、現在の状態をもとに次の変形をどうするかの「予告」を表示。以上で一行となる。
ただし、この一行の「追加」は、単にlptrを1つ増やし、内容に変更がない箇所では省略される(変更の前後で表示される)。
- もしlistの内容が単一の数値であれば、ここで終了し、ループを抜ける。
- 「予告」された処理を行う。ただし、初期状態として予告された処理は、lptrの単純な増加<→>である。
各行の表示は次のように行われます。
ステップ <-- list(lptr 以前) -->カーソル<------------ list(lptr 以降) ------------>処理予告
以上がインタープリターの動作ですが、ここで「処理予告」は、有意なもののみ記憶され、パラメータを変えた再計算時に役立てるので、コンパイラとも言えます。
ここで、普通の代入式解釈では、配列の内容を「審査」したら直ちにそれを実行するはず。
しかし、それだと、どういう状態で処理の判断がなされたかが見えないので、処理の「予告」と、実際の処理とを分け、その間に表示を入れるようにしました。
個々の「処理予告」は次の基準で。
- <→>: ここでは計算による式の簡略化は行われず、単にカーソルが移動する。
- <a→>: カーソルが移動するが、登録された変数または(Math. の)定数に数値が代入される。
- <^>: 指数計算。カーソルの直ぐ右にも"^"がある場合は直ぐ計算はしない。
- <*>: 乗算。カーソルの直ぐ右に"^"がある場合は直ぐ計算はしない。
- </>: 除算。カーソルの直ぐ右に"^"がある場合は直ぐ計算はしない。
- <+>: 加算。カーソルの直ぐ右に"^"、"*"、"/"がある場合は直ぐ計算はしない。
- <0+>式頭か"("、","(関数の第2以降引数の)の後のプラス記号。削除する。0>
- <->: 減算。カーソルの直ぐ右に"^"、"*"、"/"がある場合は直ぐ計算はしない。
- <0->式頭か"("後のマイナス記号。記号を数値に適用する。ただし、続く数値の直ぐ右に"^"がある場合は直ぐ計算はしない。0>
- <()>: 数値1個を単に囲む括弧。括弧を除く。括弧前に文字列が残る場合は(関数だから)次項に譲る。
- <f>: 括弧の終了を機に、関数を実行する。
- <.>: 1個だけの数値が得られたので、終了する。
「処理予告」のコードに続く第1引数は、カーソルの(左からの)位置。第2引数がある場合は、関数における引数の数。
さて、先に述べた「有意」な「処理予告」とは、最初の<→>を除く全てであって、これらは記憶され、つまりコンパイルされ、変数値を変えての実行がある場合、利用されます。
このやり方では次のような原則が守られます。
- 閉じる括弧")"がカーソルより左に流れることはない。
- 名前がカーソルより左に流れる場合は、関数名に限る。
計算は原則として左から順に解決しますが、次の場合は例外として右から計算されます。
- 括弧や関数など、入れ子になる場合。
- 優先度の高い計算が後に続く場合(加減算よりは乗除算、乗除算よりは指数の優先度が高い)。
- 指数計算が続く場合。例:2^1^2=2。
具体的な計算は次の場合です(表の上位の行が優先されます; n1, n2 は数値)。
配列上の位置 | lptr-pnum-1 | lptr-pnum | lptr-3 | lptr-2 | lptr-1 | lptr | 結果(赤から黄までを置換) |
配列の内容 | | | n1 | "^" | n2 | "^"以外 | n1^n2 |
| | n1 | "*", "/" | n2 | "^"以外 | n1*(/)n2 |
| | n1 | "+", "-" | n2 | "^", "*", "/" 以外 | n1+(-)n2 |
| | ",", "(", 式の冒頭 | + | n1 | | n1 |
| | ",", "(", 式の冒頭 | - | n1 | "^"以外 | -n1 |
"Math.*" | "(" | | | | ")" | 関数値 |
| | "Math.*" などでない | "(" | n1 | ")" | n1 |
| | | | n1 | 配列外 | 終了 |
計算後、lptr は、計算された数値の次を指します。
押しつけがましいようですが、コードを置いときます。