中置演算式 の評価
プログラミング言語 Gorilla がサポートする中置演算子は以下の8つです。
1 + 2;
1 - 2;
1 * 2;
1 / 2;
1 > 2;
1 < 2;
1 == 2;
1 != 2;
これらの演算子は大きく2つのグループに分かれます。上4行の四則演算は結果を整数として生成するグループで、下4つの比較を行う演算は結果を真偽値で生成するグループです。
まずは四則演算を評価できるようにしましょう。
四則演算
式の構文解析は非常に難解で実装も大変でしたが、評価はあっという間に終わります。
テストの作成
これまで通りテストの作成からです。TestEvalIntegerExpression()
にテストケースを追加します。
[TestMethod]
public void TestEvalIntegerExpression()
{
var tests = new(string, int)[]
{
// ..
("1 + 2 - 3", 0),
("1 + 2 * 3", 7),
("3 * 4 / 2 + 10 - 8", 8),
("(1 + 2) * 3 - -1", 10),
("-1 * -1", 1),
("-10 + -1 * 2", -12),
("(10 + 20) / (10 - 0)", 3),
};
// ..
}
適当に四則演算を組み合わせて優先順位が正しく計算できるかや、マイナスの数も正しく計算できるか見ておきます。括弧でのグループ式もテストしましょう。
実装
まず Eval()
に中置演算式評価用の分岐を追加し、左右両辺の式を評価します。その値を中置演算式評価用の処理に渡しその評価結果を返します。
public class Evaluator
{
// ..
public IObject Eval(INode node)
{
switch (node)
{
// ..
case InfixExpression infixExpression:
return this.EvalInfixExpression(
infixExpression.Operator,
this.Eval(infixExpression.Left),
this.Eval(infixExpression.Right)
);
}
return null;
}
}
EvalInfixExpression()
の実装は次のようになります。
public class Evaluator
{
// ..
public IObject EvalInfixExpression(string op, IObject left, IObject right)
{
// 両辺が整数オブジェクトの場合のみ
if (left is IntegerObject leftIntegerObject
&& right is IntegerObject rightIntegerObject)
{
return this.EvalIntegerInfixExpression(op, leftIntegerObject, rightIntegerObject);
}
return this.Null;
}
}
まず左右両辺のオブジェクトの型をみて、ともに整数オブジェクトであることを確認します。両辺が整数オブジェクトの場合、整数オブジェクトに対応した中置演算式評価用の処理を呼び出すようにします。
もし、左右両辺に整数オブジェクト以外が含まれる場合、Nullオブジェクトを結果をして返すようにします。つまり true + 1
のような式の評価結果は null となります。
整数値に対する中置演算を評価する処理 EvalIntegerInfixExpression()
を定義します。
public class Evaluator
{
// ..
public IObject EvalIntegerInfixExpression(string op, IntegerObject left, IntegerObject right)
{
var leftValue = left.Value;
var rightValue = right.Value;
switch (op)
{
case "+":
return new IntegerObject(leftValue + rightValue);
case "-":
return new IntegerObject(leftValue - rightValue);
case "*":
return new IntegerObject(leftValue * rightValue);
case "/":
return new IntegerObject(leftValue / rightValue);
}
return this.Null;
}
}
引数で演算対象の左右の値を IntegerObject
でもらいます。そして演算結果をもとに新たな整数オブジェクトを生成して返します。非常にシンプルな作りです。
この評価用関数に渡される時点で必ず両辺の値は整数になるようにします。両辺が整数の場合に定義されていない演算子が来た時には、Nullオブジェクトを返します。
これで実装は完了です。テストが通ります。
もちろん REPL でも動作するので計算式を入れて実行してみましょう。括弧によるグループ化した式もうまく計算してくれます。
比較演算
次は比較の演算です。まずは整数同士の比較からです。
テストの作成
これも新しいテストケースを追加することにします。真偽値の結果をテストするので TestEvalBooleanExpression()
にいくつかのテストケースを追加しましょう。
[TestMethod]
public void TestEvalBooleanExpression()
{
var tests = new(string, bool)[]
{
// ..
("1 < 2", true),
("1 > 2", false),
("1 == 2", false),
("1 != 2", true),
("1 > 2", false),
("1 < 2", true),
("1 == 1", true),
("2 != 2", false),
};
// ..
}
4津の比較についての演算子をそれぞれテストします。
実装
実装は非常に簡単です。EvalIntegerInfixExpression()
に演算子に対応した演算を追加するだけでOKです。
public class Evaluator
{
// ..
public IObject EvalIntegerInfixExpression(string op, IntegerObject left, IntegerObject right)
{
var leftValue = left.Value;
var rightValue = right.Value;
switch (op)
{
// ..
case "<":
return this.ToBooleanObject(leftValue < rightValue);
case ">":
return this.ToBooleanObject(leftValue > rightValue);
case "==":
return this.ToBooleanObject(leftValue == rightValue);
case "!=":
return this.ToBooleanObject(leftValue != rightValue);
}
return this.Null;
}
public BooleanObject ToBooleanObject(bool value) => value ? this.True : this.False;
}
三項演算子を都度書くのが面倒だったので、BooleanObject()
というメソッドを用意して真偽値オブジェクトを返すようにしました。
これで整数に対しては8つの演算子すべてに対応できました。
真偽値の比較 “==”, “!=”
このインタプリタでは真偽値の四則演算、大小の比較はありません。つまりサポートすべき演算子は == と != のみです。
テストの作成
真偽値の比較をテストします。TestEvalBooleanExpression()
にテストケースを追加しましょう。真偽値リテラル同士だけでなく式の評価結果を比較するパターンも確認します。
[TestMethod]
public void TestEvalBooleanExpression()
{
var tests = new(string, bool)[]
{
("true == true", true),
("true != true", false),
("true == false", false),
("true != false", true),
("(1 > 2) == true", false),
("(1 > 2) != false", false),
("(1 < 2) == false", false),
("(1 > 2) != true", true),
};
// ..
}
このテストはNULLが返って失敗してしまいます。
実装
中置演算式の新たな評価方法を追加するために、既存の EvalInfixExpression()
を拡張します。
public class Evaluator
{
// ..
public IObject EvalInfixExpression(string op, IObject left, IObject right)
{
if (left is IntegerObject leftIntegerObject
&& right is IntegerObject rightIntegerObject)
{
return this.EvalIntegerInfixExpression(op, leftIntegerObject, rightIntegerObject);
}
switch (op)
{
case "==":
return ToBooleanObject(left == right);
case "!=":
return ToBooleanObject(left!= right);
}
return this.Null;
}
}
switch文が新たに追加されています。ここでは比較演算子の場合、左右のオブジェクトの等価判定を行い、結果として返しています。
真偽値については true/false それぞれが同一の参照を持ちます。したがってある式で評価された true と別の式で評価された true の参照は同じのため == の判定が true となります。Null の場合も同一オブジェクトのためこの等価判定がうまく動作します。
では整数値はどうでしょうか。同じ数でも別オブジェクトとして生成されるためこの等価判定は false になってしまい 5==5 が false になるように思えます。しかし実際はこのswitch文に来る前に、整数値同士の演算として先に処理されてここまでたどり着かないので大丈夫なのです。
このことからわかるのは、整数値の比較は真偽値の比較より動作が遅くなるということです。整数の比較には型の変換やらで余計なコストがかかっているためです。
まとめ
ここまでのソース
mntm0/Gorilla at 4f238fb43d305a9454145ee69d0a88f0e8c72f59
ここまでで演算式の評価ができるようになりました。
Hello Gorilla Script!
>> 5*5 == 25
true
>> 1 + 2 > 0
true
>> true != false
true
>> -true
null
>> -true == -true
true
>> 1 + 2 * 3 / 2
4
こんな演算ができます。ちょっとした計算機のような動作を期待できます。(整数値しか扱えませんが)
このインタプリタはまだ電卓のような機能しか持ちません。もっとプログラミング言語のようにするために、次回はif式を評価できるようにしてみます。
以上。
コメントを書く