minilangの宿題をやる
「350行くらいのPythonで書くインタプリタ」の「拡張のアイデア」にある宿題をやっていきます。順番は適当です。
解説は最小限で行きます。
コメントを導入する
!
から行末までをコメントとして扱うようにしました。
Input source and enter Ctrl+D:
print 5; ! print 6;
! print 7;
print 8; ! print 9;
Output:
['program', ['print', 5], ['print', 8]]
5
8
コード中に!
を見つけたら改行(\n
)まで読み飛ばしてから次のトークンを読みに行きます。字句解析で読み飛ばしてしまうので、構文解析以降で手を入れるところはありません。
比較演算子を導入する
minilangの演算子は1文字ということにしているので、<
と>
だけ実装します。<=
と>=
はありません。そこまでこだわるところでもないんですが。
Input source and enter Ctrl+D:
print 5 + 6 < 5 * 6;
Output:
['program', ['print', ['<', ['+', 5, 6], ['*', 5, 6]]]]
true
普通に左結合の二項演算子として実装します。優先度は低め。
剰余演算子を導入する
%
で割り算の余りを計算します。比較演算子と剰余のおかげで普通の互除法が書けるようになりました。
Input source and enter Ctrl+D:
def gcd(a, b) {
var tmp = 0;
while b > 0 {
set tmp = b; set b = a % b; set a = tmp;
}
return a;
}
print gcd(36, 24);
Output:
[...]
12
普通に左結合の二項演算子として実装します。優先度はかけ算・割り算と同じにしました。0で割ることはできないので、0割りのチェックを割り算と共有しています。割り算・剰余を引数に入れて渡せるよう、operatorモジュールをimportしています。
andとorを導入する
論理積(and
)、論理和(or
)を導入します。優先度は最低。
記号にしてもよかったんですが!
をコメントに使ってしまってたので否定に使ういい感じの記号が残ってなくてPythonに寄せました。
Input source and enter Ctrl+D:
print true and false;
Output:
['program', ['print', ['and', True, False]]]
false
普通に左結合の二項演算子として実装します。というかできてしまいます。
たとえばa and b
ではまずa
を評価し、trueならばb
を評価する、逆に言うとa
がfalseならb
は見もしない、というショートカットの実装が普通です。a # 0 and b / a
のように書けばa
が0でないときだけb
をa
で割るので0割りのエラーにはなりません。関数の評価のように先に左辺も右辺も評価するとこういうチェックができませんし、無駄な計算を行うことになります。
Pythonのand
は上記のような動作を行いますので、下記の実装ではself._eval_expr(a)
がtrueだったときだけself._eval_expr(b)
が行われ、想定通りの動作となります。
case ["and", a, b]: return self._eval_expr(a) and self._eval_expr(b)
明示的に書けばこんな感じの実装になるでしょうか(正確には違います[1])。
case ["and", a, b]: return self._and(a, b)
...
def _and(self, a, b):
if not self._eval_expr(a): return False
elif self._eval_expr(b): return True
else: return False
notを導入する
否定(not
)を導入します。Python風にしたので優先度もPythonに寄せて低くしました[2]。
Input source and enter Ctrl+D:
print not true;
print not not true;
print not true or true;
Output:
[...]
false
true
true
単項演算子は初出ですが実装は簡単です。右結合なのでべき乗と同じように書けます。再帰しているのでnot not true
のようにnot
を繰り返して書けます。
単項マイナスを導入する
単項演算子を導入したので単項マイナスも導入してしまいましょう。--a
はa
を1減らすという意味ではなくて-(-a)
です。5--6
も5-(-6)
になります。
Input source and enter Ctrl+D:
print -56;
var a = 5;
print -a;
print --a;
print 5--6;
print -(a + 3);
Output:
[...]
-56
-5
5
11
-8
not
と単項マイナスは同じ構文ですので_parse_binop_left
と同様に_parse_unary
に共通部分をまとめています。
トップレベルからのreturnをエラーにする
トップレベル(関数の外側)でreturnしたらエラーにします。今まではランタイムエラーでした。
Input source and enter Ctrl+D:
return;
Output:
['program', ['return', 0]]
Error: Return at top level.
Input source and enter Ctrl+D:
評価器のいちばん外側でexcept
してつかまえてあげているだけです。最初からやっておいてもよかったかも。
breakとcontinueを導入する
while文の中でbreakとcontinueができるようにします。
Input source and enter Ctrl+D:
var n = 5;
while true {
if n = 8 { break; }
print n;
set n = n + 1;
}
print 10;
Output:
[...]
5
6
7
10
Input source and enter Ctrl+D:
set n = 5;
while n < 8 {
set n = n + 1;
if n = 7 { continue; }
print n;
}
print 10;
Output:
[...]
6
8
10
whileのなかでやることになっているいろいろをすっ飛ばしてループの外に出たり初めからやり直したりするために、return
と同様のしくみを使っています。また、スコープを作るときに退避した_env
を確実に元に戻せるよう、finallyの中に入れました。
nullを導入する
嫌われもののnullですが、optionalとかがあるわけでもないのでやっぱりnullがないとなにか足りない気がします。minilangではreturn;
と返り値を省略したときに使われるくらいです。
Input source and enter Ctrl+D:
print null;
print func() { return; }();
Output:
[...]
null
null
真偽値を追加したときと同じような作業をします。真偽値はふたつも値があるのにnullはたったひとつなのできっとかんたん!
おわりに
今回は以上です。あとは配列あたりをやろうと思ってますがすこし規模が大きくなるので別途。
Discussion