見出し画像

ゼロからはじめるスクリプト言語製作: 比較演算・論理演算を多彩に(11日目)

前回、スクリプト言語の中で整数と浮動小数の算術演算に対応することができた。今回は、if による処理の分岐や for による繰り返しをサポートする際に欠かせない、比較演算と論理演算について取り組んでいく。

ということで今回の実装ゴールを以下のように課して、実装を進めていくことにしよう。

<要件1>
1) シンボル ==、!=、<、 <=、>、>= の引数の型は、「すべて stringv である」「すべて boolv である」「すべて numberv か floatv のどちらかである」のいずれかでなければならない
2) すべての引数の評価結果が下記条件を満たしたかどうかを含む、1個の boolv 型を返す
 ・==: すべての引数が同じかどうか
 ・!=: すべての引数が同じでないかどうか
 ・<: すべての引数が小さい順かどうか(等値は認めない)
 ・<=: すべての引数が小さい順かどうか(等値を認める)
 ・>: すべての引数が大きい順かどうか(等値は認めない)
 ・>=: すべての引数が大きい順かどうか(等値を認める)

> (writeln (== "hello" "Hello") (== false true) (== 1 1. (+ 1 1)))
False False False
===> nil
> (writeln (!= "hello" "Hello") (!= false true) (!= 1 1. (+ 1 1)))
True True True
===> nil
> (writeln (< "hello" "Hello") (< false true) (<= 1 1. (+ 1 1)))
True True True
===> nil

<要件2>
3) シンボル ||、&& の引数の型は、すべて boolv でなければならない
4) すべての引数の評価結果が下記条件を満たしたかどうかを含む、1個の boolv 型を返す
 ・||: いずれかの論理値が True かどうか
 ・&&: すべての論理値が True かどうか

> (writeln (|| false false true) (|| true) (||))
True True False
===> nil
> (writeln (&& true true true) (&& true) (&&))
True True True
===> nil

<要件3>
5) シンボル ! の引数は、1個の boolv でなければならない
6) 引数の評価結果が False だったかどうかを含む、1個の boolv 型を返す

> (writeln (! false) (! true))
True False
===> nil

まずは等値判定

要件1を満たすために、まずは stringv 型同士や numberv 型同士の等しさを取得できるようにしなければならない。それぞれの基本型が等値判定に対応した型であることを示すために、IEquatable という標準の interface を導入し、Equals() 関数を実装する必要がある。
また、必須ではないもののコード利便性を考慮して、== 演算子を使った判定にも対応させておくのが良いだろう。

symbolv 型や functionv 型など等値判定に対応しない基本型も一部あるので、最初に基底クラスである expr 型に IEquatable インターフェースを導入し、次に派生クラスのそれぞれ(stringv 型、boolv 型、numberv 型、floatv 型)で Equals() 関数を override していった。

Type.cs の変更点は、↓以下のようになった。
※ 行頭「+」箇所は行追加。行頭「-」箇所は行削除。

-		public abstract class expr
+		public abstract class expr : IEquatable<expr>
		{
			:
+			public virtual bool Equals(expr? other) => throw new Exception("invalid element type");
+			// public override bool Equals(object? other) => Equals(other as expr);
+			// public override int GetHashCode() => 0;
+			public static bool operator ==(expr left, expr right) => left.Equals(right);
		}

-		public class stringv : atomv<string>
+		public class stringv : atomv<string>, IEquatable<stringv>
		{
			:
+			public bool Equals(stringv? other) => (other is null) || _val.Equals(other._val);
+			public override bool Equals(expr? other) => Equals(other?.cast<stringv>());
		}

-		public class boolv : atomv<bool>
+		public class boolv : atomv<bool>, IEquatable<boolv>
		{
			:
+			public bool Equals(boolv? other) => (other is null) || _val.Equals(other._val);
+			public override bool Equals(expr? other) => Equals(other?.cast<boolv>());
		}

-		public class numberv : atomv<long>
+		public class numberv : atomv<long>, IEquatable<numberv>
		{
			:
+			public bool Equals(numberv? other) => (other is null) || _val.Equals(other._val);
+			public override bool Equals(expr? other) => (other is null) || binaryOps(other, Equals, (floatv val, expr other) => val.Equals(other));
		}

-		public class floatv : atomv<double>
+		public class floatv : atomv<double>, IEquatable<floatv>
		{
			:
+			public bool Equals(floatv? other) => (other is null) || _val.Equals(other._val);
+			public override bool Equals(expr? other) => (other is null) || Equals(floatv.from(other));
		}

floatv 型や stringv 型には == 演算子を定義していないものの、expr 型に == 演算子を定義していることによって、floatv 型同士や stringv 型同士を == 演算子を使って等値判定できることに注意してほしい。
また算術演算で実装した numberv.binaryOps() と floatv.from() のおかげで、floatv 型と numberv 型、およびその逆の等値判定が実現できていることに注意してほしい。

これでシンボル == を実装する準備が整った。その処理本体である Core.eq() は↓以下のようなコードになった。

		public static expr reduce(expr args, expr value, expr prev, Func<expr, expr, expr, expr> func)
		{
			cell? cur = args.astype<cell>();
			if (cur is null) return value;
			expr other = cur.element().eval();
			value = func(value, prev, other);
			return reduce(cur.next(), value, other, func);
		}

		private static expr reduce0(expr args, expr value, Func<expr, expr, expr, expr> func)
		{
			cell? arg0 = args.astype<cell>();
			cell arg1 = arg0?.next().astype<cell>() ?? throw new Exception("wrong number of args");
			expr prev = arg0?.element().eval() ?? throw new Exception("invalid element type");
			return reduce(arg1 ?? args, value, prev, func);
		}

		public static expr eq(expr args) => reduce0(args, new boolv(true), (expr val, expr left, expr right) => val.cast<boolv>().and(new boolv(left == right)));

このコードの Core.reduce() は、Core.add() などの算術演算を実装するときに定義した Core.reduce() とは異なる派生版になっている。関数引数の func の型が Func<expr, expr, expr> から Func<expr, expr, expr, expr> に変化しており、渡した S 式から要素を順番に評価して func に渡すときに、その1つ手前の要素(評価結果)も一緒に渡すようになっている。

そして大小判定

等値判定が実装できれば、大小判定の実装も同じように進めることができる。
最初に基底クラスである expr 型に IComparable インターフェースを導入し、次に派生クラスのそれぞれ(stringv 型、boolv 型、numberv 型、floatv 型)で CompareTo() 関数を override していった。

Type.cs の変更点は、↓以下のようになった。
※ 行頭「+」箇所は行追加。行頭「-」箇所は行削除。

-		public abstract class expr : IEquatable<expr>
+		public abstract class expr : IEquatable<expr>, IComparable<expr>
		{
			:
+			public virtual int CompareTo(expr? other) => throw new Exception("invalid element type");
+			public static bool operator <(expr left, expr right) => left.CompareTo(right) < 0;
		}

-		public class stringv : atomv<string>, IEquatable<stringv>
+		public class stringv : atomv<string>, IEquatable<stringv>, IComparable<stringv>
		{
			:
+			public int CompareTo(stringv? other) => (other is null) ? 1 : _val.CompareTo(other._val);
+			public override int CompareTo(expr? other) => CompareTo(other?.cast<stringv>());
		}

-		public class boolv : atomv<bool>, IEquatable<boolv>
+		public class boolv : atomv<bool>, IEquatable<boolv>, IComparable<boolv>
		{
			:
+			public int CompareTo(boolv? other) => (other is null) ? 1 : _val.CompareTo(other._val);
+			public override int CompareTo(expr? other) => CompareTo(other?.cast<boolv>());
		}

-		public class numberv : atomv<long>, IEquatable<numberv>
+		public class numberv : atomv<long>, IEquatable<numberv>, IComparable<numberv>
		{
			:
+			public int CompareTo(numberv? other) => (other is null) ? 1 : _val.CompareTo(other._val);
+			public override int CompareTo(expr? other) => (other is null) ? 1 : binaryOps(other, CompareTo, (floatv val, expr other) => val.CompareTo(other));
		}

-		public class floatv : atomv<double>, IEquatable<floatv>
+		public class floatv : atomv<double>, IEquatable<floatv>, IComparable<floatv>
		{
			:
+			public int CompareTo(floatv? other) => (other is null) ? 1 : _val.CompareTo(other._val);
+			public override int CompareTo(expr? other) => (other is null) ? 1 : CompareTo(floatv.from(other));
		}

以上を踏まえて、不足している関数・シンボルを追加していき、コンパイル・デバッグしてみたのが以下の図だ。

比較演算子の実装が完了!

論理和、論理積、論理の否定

次に要件2と要件3を片付けよう。
論理値の演算は、その定義上 boolv 型のみで有効となるようにしたい。そのためそれらの演算は直接 boolv 型に定義することにして、expr 型には手を加えないということにした。例えばある boolv 型変数とある expr 型変数の論理和を求める場合は、expr 型引数が boolv 型にキャストできた場合にだけ演算を呼び出せる、ということになって都合が良い。

Type.cs の変更点は、↓以下のようになった。
※ 行頭「+」箇所は行追加。

		public class boolv : atomv<bool>, IEquatable<boolv>, IComparable<boolv>
		{
			:			
+			public boolv not() => new boolv(!_val);
+			public boolv or(boolv other) => new boolv(_val || other._val);
+			public boolv and(boolv other) => new boolv(_val && other._val);
+			public expr or(expr other) => or(other.cast<boolv>());
+			public expr and(expr other) => and(other.cast<boolv>());
		}

これでシンボル ! || && を実装する準備が整った。その処理本体である Core.not()・Core.or()・Core.and() は↓以下のようなコードになった。

		public static expr not(expr args)
		{
			cell? arg0 = args.astype<cell>();
			cell? arg1 = arg0?.next().astype<cell>();
			if (arg1 is not null) throw new Exception("wrong number of args");
			expr value = arg0?.element().eval() ?? throw new Exception("invalid element type");
			return value.cast<boolv>().not();
		}

		public static expr or(expr args) => reduce(args, new boolv(false), (expr left, expr right) => left.cast<boolv>().or(right));
		public static expr and(expr args) => reduce(args, new boolv(true), (expr left, expr right) => left.cast<boolv>().and(right));

コンパイル・デバッグしてみたのが以下の図だ。

論理演算子の実装が完了!

今日はここまで、おつかれさま。
Program.cs  は計 81 行、Type.cs  は計 239 行、Core.cs は 131 行。

ブラウザー実行環境 replit のご紹介

この連載では、製作中のコードの変更点を断片的に引用して提示しているだけなので、読者目線ではプログラムの全体像が分かりづらかったかもしれない。
ということで、今日までの成果を実際に動作させて、ブラウザー上で確認できるような環境を準備してみた。

興味があれば下記リンク先で「Run」ボタンをクリックしてみてほしい。

「Run」ボタンをクリック後に、小町算を入力して、実行させたところ

今日の時点で、算術演算・比較演算・論理演算の組み合わせに対応することができているので、ユーザーとしていろいろ入力して、実際に実行させてみてほしい。
「Show files」ボタンをクリックすると、コード全体がどんな構成になっているのかを確認することもできる。

さて次回からは、制御構文や変数宣言の実装に移っていこうと思っている。
乞うご期待。


この記事が気に入ったらサポートをしてみませんか?