見出し画像

ゼロからはじめるスクリプト言語製作: 変数を定義する(12日目)

前回の実装を終えて、ユーザーは算術演算や論理演算を自由に組み合わせた計算ができるようになった。今回は、計算の幅を大きく広げるために欠かせない、変数の概念をスクリプト言語に組み込んでいく。

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

<要件1>
1) シンボル ' は複数の引数を受け取ることができ、評価した場合は ' に続くすべての引数を評価せず、S 式としてそのまま返す
2) ' で始まるシンボルは、' を取り除いた名前を持つ symbolv を返す

> (' 1 2 (+ 1 2))
===> 1 2 (+ 1 2)
> (writeln 'x 'y 'z)
x y z
===> nil

<要件2>
3) シンボル $ は2つの引数を取り、1つめは symbolv 型、2つめは expr 型でなければならない
4) 1つめの symbolv はグローバルな変数名を示し、それを評価したときに2つめの expr を返す

> ($ 'x 5)
===> x
> (writeln (% x 5) (% x 3))
0 2
===> nil

評価のエスケープ処理

まず要件1の 1) をやっつけよう。Lisp や Clojure では quote と表現される処理だ。与えられた S 式の引数をそれぞれ評価せずに返すだけなので、悩むことはない。

Core.quote() は↓以下のようなコードになった。

		public static expr quote(expr args)
		{
			return args;
		}

要件1の 2) は symbolv 型の評価の仕方を変更することで実装できる。これも悩むことはない。

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

		public class symbolv : atomv<string>, IDisposable
		{
			:
-			public override expr eval() => _scope.GetValueOrDefault(_val) ?? throw new Exception($"unknown symbol [{_val}]");
+			public override expr eval() => !_val.Equals("'") && _val.StartsWith("'") && !_val.StartsWith("''") ? new symbolv(_val.Substring(1)) : _scope.GetValueOrDefault(_val) ?? throw new Exception($"unknown symbol [{_val}]");
			:
		}

これで要件1を満たすことができた。
' のシンボル定義を追加して、コンパイル&デバッグしたのが下図である。
※ 行頭「+」箇所は行追加。

		static Core()
		{
			new symbolv("writeln").assign(new functionv(Core.writeln));
			new symbolv("+").assign(new functionv(Core.add));
			new symbolv("-").assign(new functionv(Core.sub));
			:
+			new symbolv("'").assign(new functionv(Core.quote));
		}
エスケープ処理に対応できた!(でもこれが何の役に…?)

そして変数代入へ

次に要件2を片付けていこう。1つめの引数を評価した結果が symbolv 型になった場合は、2つめの引数の評価結果をそのシンボルに対する評価結果として設定したい。これはすでに実装されている symbolv.assign() を呼び出すことにすればよい。

Core.assign() のコードは、↓以下のようになった。

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

変数 arg0~2 はそれぞれ引数の 0~2 個めを指しており、arg2 が存在せず、arg0 の評価結果 key が symbolv 型であり、arg1 の評価結果 value が expr 型であることを要求している。

$ のシンボル定義を追加して、コンパイル&デバッグしたのが下図である。
※ 行頭「+」箇所は行追加。

		static Core()
		{
			new symbolv("writeln").assign(new functionv(Core.writeln));
			new symbolv("+").assign(new functionv(Core.add));
			new symbolv("-").assign(new functionv(Core.sub));
			:
			new symbolv("'").assign(new functionv(Core.quote));
+			new symbolv("$").assign(new functionv(Core.assign));
		}
変数 x を宣言して、参照することができた!

今日はここまで、おつかれさま。
Program.cs は計 81 行、Type.cs は計 239 行、Core.cs は 149 行。
変数が扱えるようになった割には、コード量が 18 行増えただけで済んだ。なんか、感動する。


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