トイ言語実験日記4(テーマ未定)
大きなネタはいったん尽きてる感じでどんなテーマで続けるか決まっていない
決まったら書き換える
ひさびさにmainブランチに戻ってクラスで書き換えてみた
Evaluatorにインスタンス変数がないとただself
をあちこちに書くだけになってしまうのでself._env
に環境を持たせてみた
メソッド呼び出しが少し短くはなるけど、_apply
でself._env
を自分で元に戻してやらないといけなくなった
いまひとつかなー
Pythonってapplyがないんだよなと思ってたけど考えてみたらf(*args)
って書けばおんなじことだったのでついでに書き換えた
def _apply(self, f_val, args_val):
if callable(f_val):
return f_val(*args_val)
組み込み関数がちょっとすっきりする
class Interpreter:
def __init__(self):
self._env = Environment()
_builtins = {
"__builtins__": None,
"add": operator.add,
"sub": operator.sub,
"equal": operator.eq,
"print": print,
}
...
やっぱり環境をオブジェクトに持つのは気持ち悪いので引数で渡すことにした。
Evaluatorクラスはメソッドを隠すくらいしか意味がなくなったけど。
毎回env
を引数に入れるのは面倒といえば面倒だけれどもapply
がとてもストレートに読めるのがうれしい。
使用前
def _apply(self, f_val, args_val):
if callable(f_val):
return f_val(*args_val)
_, params, body, env = f_val
prev_env = self._env
self._env = env.extend(params, args_val)
val = self.eval(body)
self._env = prev_env
return val
使用後
def _apply(self, f_val, args_val):
if callable(f_val):
return f_val(*args_val)
_, params, body, env = f_val
return self.eval(body, env.extend(params, args_val))
さらに、原理を見せるという意味ではextend
も直接apply
に書いてしまう方がわかりやすい気がした。
def _apply(self, f_val, args_val):
if callable(f_val):
return f_val(*args_val)
_, params, body, env = f_val
new_env = Environment(env)
for param, arg in zip(params, args_val):
new_env.define(param, arg)
return self.eval(body, new_env)
ほかにも少々修正。
そもそもextend
をEnvironment
のメソッドにしていたのがおかしかったかもしれない。Evaluator
のメソッドにするべきだったかな。
この評価器を使うのに必要な分だけの字句解析・構文解析を付けた。
いままでどおりのことをしているだけだけれどもメソッドを並べる順番を変えてみた。
パブリックなメソッドを上に置くのは今までどおりだけれどもその他のメソッドは呼ぶ方を上に、呼ばれる方を下にした。
関数だけで書いてた時は呼ばれる方を上に書かざるを得ないのでそういう順番になってたんだけれどもクラスに書き換えたのでそのへんは制限がなくなったし。
エラーメッセージに行番号を出せるようにしよう。
短い式をひとつずつrun()
で書いているうちは今のままでも案外困らないんだけれども、コードが長くなってくると徐々につらくなってくる。
i.go("""
fib := func (n) do
if n == 0 then 0
elif n == 1 then 1
else fib(n - 1) + fib(n - 2) end
end;
print(fib(10))
""")
今までトークンは"foo"
, 5
, 生のデータで書いていたけれどもこれに行番号の情報を追加する必要があるのでクラスを作る。
けっこう全面的に手を入れないといけない気がする。
なのでこのタイミングでmainからブランチ分けて進めようという気になった。
うまくいきそうだったら進めてる方にも実装する。
もしかしたら["token", "foo", 10]
のように配列で持たせた方がマクロとの相性がいいかもしれない。
こまってから考える。
とりあえずToken
クラスはこんな感じで。
import dataclasses
@dataclasses.dataclass
class Token:
val: None | bool | int | str | list
text: str
line: int
今までnext_token()
で返してたものがval
に入り、text
はもとのソースそのままの文字列を入れる。
エラーメッセージをこんな風に出そうとしているので、もとの文字列も取っておこうかと。
ソースには+
って書いてあるけどASTでは"add"
になる、というのもあるので。
from typing import NoReturn
def report_error(msg, text, line) -> NoReturn:
if text == "$EOF":
raise AssertionError(msg + f" at end")
else:
raise AssertionError(msg + f": `{text}` at line {line}")
NoReturnにしておかないとNoneが返るものと判定されてしまって警告がうるさい。
Scanner
から手を付ける。
行番号を覚えておくようにする。
class Scanner():
def __init__(self, src):
self._src = src
self._pos = 0
self._line = 1
self._text = ""
行番号は_advance()
で数える。
def _advance(self):
if self._current_char() == "\n":
self._line += 1
self._pos += 1
トークンを作るユーティリティメソッド。
def _token(self, val):
return Token(val, self._text, self._line)
あとはトークンを返すところで_token()
でトークンを作ったり($EOF
のときだけちょっと特殊)知らない文字が現れたらエラーを出したり。
def next_token(self):
while self._current_char().isspace(): self._advance()
self._text = ""
match self._current_char():
case "$EOF":
return Token("$EOF", "$EOF", self._line)
case c if is_name_first(c):
return self._name()
case c if c.isnumeric():
self._word(str.isnumeric)
return self._token(int(self._text))
case c if c in "=:":
self._append_char()
if self._current_char() == "=": self._append_char()
return self._token(self._text)
case c if c in "+-(),;":
self._append_char()
return self._token(self._text)
case c:
report_error("Unexpected character", c, self._line)
軽く試す。
if __name__ == "__main__":
def scan(src):
scanner = Scanner(src)
while True:
token = scanner.next_token()
print(token)
if token.val == "$EOF": break
print()
scan("""
fib := func (n) do
if n == 0 then 0
elif n == 1 then 1
else fib(n - 1) + fib(n - 2) end
end;
print(fib(10))
""")
大丈夫そうだ。行も数えてる。
Token(val='fib', text='fib', line=2)
Token(val=':=', text=':=', line=2)
Token(val='func', text='func', line=2)
Token(val='(', text='(', line=2)
Token(val='n', text='n', line=2)
:
Token(val='fib', text='fib', line=8)
Token(val='(', text='(', line=8)
Token(val=10, text='10', line=8)
Token(val=')', text=')', line=8)
Token(val=')', text=')', line=8)
Token(val='$EOF', text='', line=9)
エラーも出る。
scan("""
@
""")
もうAssersionErrorじゃなくていいのかもしれないが。
AssertionError: Unexpected character: `@` at line 2
Parser
をやっていく。
なんとなく._current_token
とかで隠蔽されているので「トークンを渡す」みたいなところはそのままで大丈夫だけれどもトークンの中身を見ているところはじょりじょり変更していく必要がある。
たとえばここではself._current_token.val
を見ている。
def parse(self):
expr = self._expression()
if self._current_token.val != "$EOF":
self._report_error("Unexpected token at end")
return expr
エラーはたいてい(いつも?)トークンの情報を使って表示するはずなのでユーティリティメソッドを作った。
def _report_error(self, msg):
report_error(msg, self._current_token.text, self._current_token.line)
.val
と.text
がたいていの場合同じなので、どっちを使うべきか迷うが、基本的にはこれまでトークンとして扱っていたものが.val
に入ってるから.val
メインで.text
は補助と考える。
ただし_binary_left()
と_binary_right()
では.text
を見て.val
を変える、ということにした。
まだ考え方の整理が必要かもしれない。
ていねいにやるとさらに.type
みたいなプロパティをつけるんだと思うがあまり手を広げないようにする。
def _binary_left(self, ops, sub_elem):
left = sub_elem()
while (op := self._current_token).text in ops:
self._advance()
left = [op.with_val(ops[op.text]), left, sub_elem()]
return left
def _binary_right(self, ops, sub_elem):
left = sub_elem()
if (op := self._current_token).text in ops:
self._advance()
return [op.with_val(ops[op.text]),
left, self._binary_right(ops, sub_elem)]
return left
ここでop.with_val(ops[op.text])
が何をやっているのかというと、op
の.val
だけ変更したあたらしいToken
オブジェクトを返している。
たとえばToken(val='+', text='+', line=5)
を受け取ったらToken(val='add', text='+', line=5)
を返す。
Token(val='add', text='+', line=5)
ではなくただの"add"
でもいけるのでは?という気もしたがたぶん統一されてたほうがいいだろう。
評価中のエラー表示できっと必要になる。
dataclasses
モジュールにreplace
というそのものずばりのメソッドがあるのを知った。
オブジェクトを直接更新できないようfrozen=True
もつけてみた。
@dataclasses.dataclass(frozen=True)
class Token:
...
def with_val(self, val):
return dataclasses.replace(self, val=val)
動かしてみる。
if __name__ == "__main__":
import pprint
pprint.pprint(Parser("""
fib := func (n) do
if n == 0 then 0
elif n == 1 then 1
else fib(n - 1) + fib(n - 2) end
end;
print(fib(10))
""").parse())
これくらいになってくるとさすがにpprint
じゃないとつらい。
というかpprint
でもつらい。
[Token(val='seq', text=';', line=6),
[Token(val='define', text=':=', line=2),
Token(val='fib', text='fib', line=2),
[Token(val='func', text='func', line=2),
[Token(val='n', text='n', line=2)],
[Token(val='if', text='if', line=3),
[Token(val='equal', text='==', line=3),
Token(val='n', text='n', line=3),
Token(val=0, text='0', line=3)],
Token(val=0, text='0', line=3),
[Token(val='if', text='elif', line=4),
[Token(val='equal', text='==', line=4),
Token(val='n', text='n', line=4),
Token(val=1, text='1', line=4)],
Token(val=1, text='1', line=4),
[Token(val='add', text='+', line=5),
[Token(val='fib', text='fib', line=5),
[Token(val='sub', text='-', line=5),
Token(val='n', text='n', line=5),
Token(val=1, text='1', line=5)]],
[Token(val='fib', text='fib', line=5),
[Token(val='sub', text='-', line=5),
Token(val='n', text='n', line=5),
Token(val=2, text='2', line=5)]]]]]]],
[Token(val='print', text='print', line=8),
[Token(val='fib', text='fib', line=8), Token(val=10, text='10', line=8)]]]
ちょっといじるとエラーも出る。
AssertionError: `)` expected at end
if文ではelif
をif
の入れ子で表現しているので.val
を"if"
に変更してASTにするとかしている。
エラーを出力するときには.text
が使われるのでelif
でエラーだよと言ってくれるはず。
今そういうエラーは存在しない気はするけど。
構文解析中のエラーはほとんど_consume()
でしか出してないから。
わかりやすいエラーメッセージを出そうとするといろいろ工夫しないといけなさそう。
else
が省略された時のNone
もトークンの形にしておく必要がある。.text
や.line
はもう適当(おい
def _if(self):
op = self._advance().with_val("if")
cnd = self._expression()
self._consume("then")
thn = self._expression()
if self._current_token.val == "elif":
return [op, cnd, thn, self._if()]
if self._current_token.val == "else":
self._advance()
els = self._expression()
self._consume("end")
return [op, cnd, thn, els]
self._consume("end")
return [op, cnd, thn, Token(None, "", op.line)]
以前はif
のトークンを_advance()
して読みとばした状態で_if()
を呼ぶようにしていたけれども、_if()
の中で使いたいので_if()
に入ってから_advance()
するようにしている。
func
も同じ。
考え方をそろえてないとわけわからなくなりそう。
Environment
はいじらなくていいかなあ・・・
たとえばここで、name
で文字列じゃなくてトークンを受け取って.val
で探す、みたいなこともできると思うけど。
def define(self, name, val):
self._vals[name] = val
return val
もっと言えばトークンそのものをキーにして覚えておくこともできなくはないと思うけど。
いじらずに進む。
Environment
がトークンの構造を知らなくて済めばその方が幸せだと思うので。
困ったら考える(そればっかり
Evaluator
はそこらじゅうToken
を見たりいじったりするようにしなきゃいけなくて面倒そうだなあ・・・と思ってたけど思ってたほどではなかった。
eval()
のパターンマッチはさすがに面倒な感じだけれどもeval()
以外はいじるところがなくて拍子抜けって感じ。
もともと短いというのももちろんある。
class Evaluator:
def eval(self, expr, env):
match expr:
case Token(val=v):
return env.get(v) if isinstance(v, str) else v
case [Token(val="func"), params, body]:
return ["func", params, body, env]
case [Token(val="define"), name, val]:
return env.define(name.val, self.eval(val, env))
case [Token(val="assign"), name, val]:
return env.assign(name.val, self.eval(val, env))
case [Token(val="seq"), *exprs]:
return self._eval_seq(exprs, env)
case [Token(val="if"), cnd, thn, els]:
return self._eval_if(cnd, thn, els, env)
case [func, *args]:
return self._apply(
self.eval(func, env),
[self.eval(arg, env) for arg in args])
case unexpected:
report_error("Unexpected expression", unexpected, 0)
if
だとかfunc
だとかをひとつひとつクラスにしたらパターンマッチはもうちょっとキレイになるだろう。
クラス定義だけで行が増えていくのはあんまりうれしくないけど本格的な処理系を書くようになったらそういうのしてったほうがいいんだろうね。
Evaluator
の中で唯一エラーを出しているのもここ。
ただどんな形をしているのかわからないのでどこでエラーになっているのか出しにくい。
expr
をまるごと出力して行番号は0にした。
ここを通るのはバグを作りこんだってことなのでまあいいか。
たとえば define
で name.val
が str
じゃなかった場合をチェックする、みたいなのだったらちゃんと出せる。はず。
テストも全部通った。
これ、マクロでやったらどうなるんだろう
単純に同じようにやっていけば動くのは動くと思うんだけれども
マクロで出力したASTの中でエラーがあったらどうなるんだろう?
マクロの引数で受け取った、もとのソース内にあるコードならそのままでいいか
マクロ内でquoteしたものはマクロ定義の方の行でレポートされるけどそれもそれでわるくはないか
やればいいだけ?
いや、そもそもマクロでToken
オブジェクト作らないといけない?できる?
q
やqq
に手を入れたら何とかなる?
わからないのでやってみよう!
問題はどのソースから始めるかだな
小さくやりたいのでCPS版とかステートマシン版とかはおいとくとして
初期にマクロまで実装してたやつに行番号をつけるか
今いじってるコードにマクロをつけるか
このまま続けるかな
気分で
配列関連とq()
とmacro()
くらい実装すれば原理は試せるだろう
ちょっとその前にこれを決着つけたくなった
Environmentはいじらなくていいかなあ・・・
...
Environmentがトークンの構造を知らなくて済めばその方が幸せだと思うので。
Environmentクラスがトークンを知らないとエラー時に行番号を出せないことに気づいた
今はこう
assert False, f"Undefined variable: `{name}` @ assign."
やっぱり行番号も出したいよね、ということで考える
Environmentにトークンを渡すようにしてみた
渡すだけで辞書のキーは文字列を取り出して使い、トークンとしてはエラーの時にだけ使う
class Environment:
def __init__(self, parent=None):
self._parent = parent
self._vals = {}
def define(self, name, val):
self._vals[name.val] = val
return val
def assign(self, name, val):
if name.val in self._vals:
self._vals[name.val] = val
return val
elif self._parent is not None:
return self._parent.assign(name, val)
else:
self._report_error(f"Undefined variable", name)
def get(self, name):
if name.val in self._vals:
return self._vals[name.val]
elif self._parent is not None:
return self._parent.get(name)
else:
self._report_error(f"Undefined variable", name)
def _report_error(self, msg, name):
report_error(msg, name.text, name.line)
呼び出す側はname.val
じゃなくてname
を渡せばよいのですっきりするといえばするんだけれどもなんか嫌だなあ
特に組み込み関数の初期化
class Interpreter:
def init_builtins(self):
_builtins = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
"equal": lambda a, b: a == b,
"print": print
}
for name, func in _builtins.items():
self._env.define(Token(name, name, 0), func)
self._env = Environment(self._env)
わざわざトークン作って渡すとかないよねー
別の方法を考える
失敗したときにNone
とかFalse
とかを返す方法は使えないので、真っ先に思い付くのは例外を投げること
class VariableNotFoundError(Exception): pass
class Environment:
...
def assign(self, name, val):
if name in self._vals:
self._vals[name] = val
return val
elif self._parent is not None:
return self._parent.assign(name, val)
else:
raise VariableNotFoundError()
def get(self, name):
if name in self._vals:
return self._vals[name]
elif self._parent is not None:
return self._parent.get(name)
else:
raise VariableNotFoundError()
...
class Evaluator:
def eval(self, expr, env):
match expr:
...
case Token(val=v):
try:
return env.get(v) if isinstance(v, str) else v
except VariableNotFoundError:
self._report_error(f"Variable not found", expr)
...
case [Token(val="assign"), name, val]:
try:
return env.assign(name.val, self.eval(val, env))
except VariableNotFoundError:
self._report_error(f"Variable not found", name)
...
Python的にはこれかなあ
あるいはNone
でもFalse
でもほかのどんな値でもない失敗を表す値を返すこと
Option型のNone
のイメージで、でもNone
じゃないものということでNothing
という名前にしてみた。
型ヒントをつけるならValueType | Nothing
っていうイメージ(イメージだけ
Nothing = object()
class Environment:
...
def assign(self, name, val):
if name in self._vals:
self._vals[name] = val
return val
elif self._parent is not None:
return self._parent.assign(name, val)
else:
return Nothing
def get(self, name):
if name in self._vals:
return self._vals[name]
elif self._parent is not None:
return self._parent.get(name)
else:
return Nothing
...
class Evaluator:
def eval(self, expr, env):
match expr:
case Token(val=v):
if isinstance(v, str) :
if (var_val := env.get(v)) is not Nothing:
return var_val
else:
self._report_error(f"Variable not found", expr)
else:
return v
...
case [Token(val="assign"), name, val_expr]:
val = self.eval(val_expr, env)
if env.assign(name.val, val) is not Nothing:
return val
else:
self._report_error(f"Variable not found", name)
...
思ってたほどすっきりしなかった。
例外利用のパターンで進めよう。
q
だけやってみる
class Evaluator:
def eval(self, expr, env):
match expr:
...
case [Token(val="q"), elem]:
return elem
...
で
i.go("""
print(q(a + b))
""")
を実行する
[Token(val='add', text='+', line=2), Token(val='a', text='a', line=2), Token(val='b', text='b', line=2)]
そういうことだよな
これで print(q(hello))
でちょっとした文字列の代わりに使えますとか言えなくなってしまったな
いや、print()
を定義しなおす方法もあるか?
まあちょっとおいておこう
少なくともデバッグ中はこっちのほうが便利だろう
マクロ処理のためのしくみを入れていく。
最低限なにを書けば動くかな・・・
class Parser:
...
def _primary(self):
match self._current_token.val:
...
case "func" | "macro":
return self._func_macro()
...
def _func_macro(self):
op = self._advance()
...
return [op, params, body]
...
class Evaluator:
def eval(self, expr, env):
match expr:
...
case [Token(val="macro"), params, body]:
return ["macro", params, body, env]
...
case [op, *args]:
return self._eval_op(op, args, env)
...
...
def _eval_op(self, op, args, env):
match self.eval(op, env):
case ["macro", params, body, menv]:
return self.eval(self._expand(body, params, args, menv), env)
case f_val:
return self._apply(
f_val,
[self.eval(arg, env) for arg in args])
def _expand(self, body, params, args, menv):
new_menv = self._extend(menv, params, args)
return self.eval(body, new_menv)
def _apply(self, f_val, args_val):
if callable(f_val):
return f_val(*args_val)
_, params, body, env = f_val
new_env = self._extend(env, params, args_val)
return self.eval(body, new_env)
def _extend(self, env, params, args_val):
new_env = Environment(env)
for param, arg in zip(params, args_val):
new_env.define(param.val, arg)
return new_env
一見動いた風
i.go("""
foo := macro(a, b) do arr(q(add), a, b) end;
print(foo(5 + 6, 7 + 8))
""")
→ 26
でも実はarr(q(add), a, b)
のa
を5に変えるだけでエラー
5
は評価すると単なる数値の5になってしまい、5
というトークンではないので
あとif文とかも作れない
q
の中身はちゃんと構文解析を通る形でないといけないのでq(if)
のようにはできない
"+"
の代わりに"add"
と書かないといけない
まあこれはそう書けばいいだけだが
ということでトークンを作る仕組みとqq
は必要そう
あとexpand
もほしいな
expand
作ってみたけどエラーの出し方がやっぱり困るな・・・
def _eval_expand(self, op, args, env):
match self.eval(op, env):
case ["macro", params, body, menv]:
return self._expand(body, params, args, menv)
case unexpected:
report_error("Macro expected", unexpected, 0)
expand
のトークンから.text
と.line
を持ってくる?
op
からトークンを探す?
あとで考えることにする
foo := macro(a, b) do arr(q(add), a, b) end;
print(expand(foo(5 + 6, 7 + 8)))
でこう出力される(エディタにて整形済み
[
Token(val="add", text="add", line=2),
[
Token(val="add", text="+", line=3),
Token(val=5, text="5", line=3),
Token(val=6, text="6", line=3),
],
[
Token(val="add", text="+", line=3),
Token(val=7, text="7", line=3),
Token(val=8, text="8", line=3),
],
]
expandしてくれてはいる
qq
を実装して・・・
(ここまでくると全部できあがったやつに行番号を実装しなおした方が速いかも・・・)
def _eval_quasiquote(self, expr, env):
def qqelems(elems):
quoted = []
for elem in elems:
match elem:
case [Token(val="!!"), e]:
vals = self.eval(e, env)
assert isinstance(vals, list), f"Cannot splice in quasiquote: {e}"
quoted += vals
case _: quoted.append(self._eval_quasiquote(elem, env))
return quoted
match expr:
case [Token(val="!"), elem]: return self.eval(elem, env)
case [*elems]: return qqelems(elems)
case _: return expr
あとScanner
とかEvaluator.eval()
も直したけど省略
これは動く
myif := macro(cnd, thn, els) do qq(if !(cnd) then !(thn) else !(els) end) end;
print(expand(myif(5 == 5, 7 + 8, 9 + 10)));
print(myif(5 == 5, 7 + 8, 9 + 10));
print(myif(5 == 6, 7 + 8, 9 + 10))
↓
[Token(val='if', text='if', line=2), [Token(val='equal', text='==', line=3), Token(val=5, text='5', line=3), Token(val=5, text='5', line=3)], [Token(val='add', text='+', line=3), Token(val=7, text='7', line=3), Token(val=8, text='8', line=3)], [Token(val='add', text='+', line=3), Token(val=9, text='9', line=3), Token(val=10, text='10', line=3)]]
15
19
これは動・・・く!?
if_five := macro(cnd, thn, els) do qq(if !(cnd) == 5 then !(thn) else !(els) end) end;
print(expand(if_five(5 == 5, 7 + 8, 9 + 10)));
print(if_five(5, 7 + 8, 9 + 10));
print(if_five(6, 7 + 8, 9 + 10))
↓
[Token(val='if', text='if', line=2), [Token(val='equal', text='==', line=2), [Token(val='equal', text='==', line=3), Token(val=5, text='5', line=3), Token(val=5, text='5', line=3)], Token(val=5, text='5', line=2)], [Token(val='add', text='+', line=3), Token(val=7, text='7', line=3), Token(val=8, text='8', line=3)], [Token(val='add', text='+', line=3), Token(val=9, text='9', line=3), Token(val=10, text='10', line=3)]]
15
19
そうか、5
も評価しないでトークンのまま使ってるから・・・
なんかうまくいっちゃうもんだねえ
なんでさっきは?
でも実はarr(q(add), a, b)のaを5に変えるだけでエラー
5は評価すると単なる数値の5になってしまい、5というトークンではないので
そうか、5じゃなくてq(5)
にしてやらないといけなかったんだな
foo := macro(a, b) do arr(q(add), q(5), b) end;
print(foo(5 + 6, 7 + 8))
これならエラーにならなかった
expand作ってみたけどエラーの出し方がやっぱり困るな・・・
あとで考えることにする
どうしようか
困る場合はふたつありそう
- トークンを受け取ってない
- トークンを含む情報はもらっているが形が決まってなくてどこにあるかわからない
受け取ってないのはどうしようもないので引数かプロパティかで渡すしかない
引数で渡してみよう
形が決まってない場合は適当に探す?
- トークンを含む情報はもらっているが形が決まってなくてどこにあるかわからない
こっちからやる
式も受け取れるようにして、一番左にあるトークンを探すようにした
全部探そうとはしてなくてわりとすぐあきらめてUnknown
でしたーと投げ出す
def _report_error(self, msg, expr):
def first_token(expr):
match expr:
case Token():
return expr
case [first, *_rest]:
return first_token(first)
case _:
return Token("Unknown", "Unknown", 0)
token = first_token(expr)
report_error(msg, token.text, token.line)
これをこんな風に呼ぶ
def _eval_expand(self, op, args, env):
match self.eval(op, env):
case ["macro", params, body, menv]:
return self._expand(body, params, args, menv)
case unexpected:
self._report_error("Macro expected", op)
expand(5 + 6)
を評価すると
AssertionError: Macro expected: `+` at line 1
と出してくれる
わかりやすいかはともかくとしてその行だということはわかる
- トークンを受け取ってない
ところを探してみたけどそういうところで出してるエラーがなかった!
ので新たに作る
このへんでどうだ
_applyには関数も引数も評価済みの値の状態で渡されるので何に由来するのかわからない
def _apply(self, f_val, args_val):
if callable(f_val):
return f_val(*args_val)
...
print(5 + arr(6, 7))
を実行すると生の例外で止まってしまう
TypeError: unsupported operand type(s) for +: 'int' and 'list'
そこで例外を拾ってエラーメッセージを出すようにする
def _apply(self, op, f_val, args_val):
if callable(f_val):
try:
return f_val(*args_val)
except TypeError:
self._report_error("Type error", op)
...
op
は上から渡してもらっている
def _eval_op(self, op, args, env):
match self.eval(op, env):
case ["macro", params, body, menv]:
return self.eval(self._expand(body, params, args, menv), env)
case f_val:
return self._apply(
op, f_val,
[self.eval(arg, env) for arg in args])
しかしそのためにop
を持ちまわるのかというとなんとかならないかなあ
例外をスルーしてトークン持ってる人のところまで戻るっていう手があるか
_apply()
は元に戻して呼び出し元でエラーを出してもらう
def _eval_op(self, op, args, env):
match self.eval(op, env):
...
case f_val:
try:
return self._apply(
f_val,
[self.eval(arg, env) for arg in args])
except TypeError:
self._report_error("Type error", op)
このほうがよさそうだ
5 + arr(6, 7)
を評価すると
Type error: `+` at line 1
わかりやすいエラーメッセージを!ってやってるといくらでも沼がありそうなのでこれくらいにしておく
実用に耐える言語を作るときはこの沼と正面から立ち向かうんだろうねえ
原理上はトークン持ち回りで情報は出せそう
逆に末端でないとわからない情報は例外に入れて持ち上げる
例外に入れて持ち上げる方法はCPSでは通用しないかもっていう心配は少しある
文字列型を作るよ
文字列型を作るときの問題は、今単に文字列をトークンとして扱うとそれは文字列ではなくて名前と解釈されてしまうこと
None
やTrue
、False
、5
などは値をそのまま使っているので(今はTokenクラスに囲まれてるけど)その流儀で行く。
そのために、今名前として処理しているものをクラスとして扱う。
変数名だけじゃなくてif
とか;
とかもだからちょっと違和感あるけど。
あとToken
型に入れられるようにする。
@dataclasses.dataclass(frozen=True)
class Name:
name: str
@dataclasses.dataclass(frozen=True)
class Token:
val: None | bool | int | str | list | Name
text: str
line: int
strを継承させる(class Word(str): pass
)という手もあるだろうけど、いろいろ操作する必要は多分ないし、Wordオブジェクトがstrにマッチしたりして紛らわしいので別物にしよう(判定の順序でかわせるかもしれないが)。
毎回.name
って書かなきゃいけないのはめんどうだけど。
Scanner
では名前を読んだらName
にしてからトークンにする。
def next_token(self):
...
match self._current_char():
...
case c if c in "=:":
self._append_char()
if self._current_char() == "=": self._append_char()
return self._token(Name(self._text))
case c if c in "+-(),;":
self._append_char()
return self._token(Name(self._text))
...
def _name(self):
self._word(is_name_rest)
match self._text:
case "None": return self._token(None)
case "True": return self._token(True)
case "False": return self._token(False)
case text : return self._token(Name(text))
Parser
ではひたすら文字列をName
でくるむ。
_binary_right
等では呼び出された側でくるんでいるので呼び出し側は変更しない。
def _sequence(self):
expr = self._define_assign()
if self._current_token.val != Name(";"):
return expr
else:
op = self._current_token.with_val(Name("seq"))
seq = [op, expr]
while self._current_token.val == Name(";"):
self._advance()
seq.append(self._define_assign())
return seq
def _define_assign(self):
return self._binary_right({
":=": "define", "=": "assign"
}, self._comparison)
以下省略。
Evaluator
も同様。
意外と局所的。関数2か所で済んだ。
def eval(self, expr, env):
match expr:
case Token(Name(name)):
try:
return env.get(name)
except VariableNotFoundError:
self._report_error(f"Variable not found", expr)
case Token(val=v):
return v
case [Token(val=Name("q")), elem]:
return elem
case [Token(val=Name("qq")), elem]:
return self._eval_quasiquote(elem, env)
case [Token(val=Name("func")), params, body]:
return ["func", params, body, env]
case [Token(val=Name("macro")), params, body]:
return ["macro", params, body, env]
case [Token(val=Name("define")), name, val]:
return env.define(name.val.name, self.eval(val, env))
case [Token(val=Name("assign")), name, val]:
try:
return env.assign(name.val.name, self.eval(val, env))
except VariableNotFoundError:
self._report_error(f"Variable not found", name)
case [Token(val=Name("seq")), *exprs]:
return self._eval_seq(exprs, env)
case [Token(val=Name("if")), cnd, thn, els]:
return self._eval_if(cnd, thn, els, env)
case [Token(val=Name("expand")), [op, *args]]:
return self._eval_expand(op, args, env)
case [op, *args]:
return self._eval_op(op, args, env)
case unexpected:
self._report_error("Unexpected expression", expr)
...
def _extend(self, env, params, args_val):
new_env = Environment(env)
for param, arg in zip(params, args_val):
new_env.define(param.val.name, arg)
return new_env
きっとこれで文字列型導入の準備OK。
さてでは。
といってもこれだけ。
文字列をシングルクォートで囲むようにしたのは、Pythonのコード中に書きやすいから。他意はない。
def next_token(self):
...
match self._current_char():
...
case c if c == "'":
self._advance()
while (c := self._current_char()) != "'":
if c == "$EOF":
report_error("Unterminated string", self._text, self._line)
self._append_char()
self._advance()
return self._token(self._text)
...
print('hello, world')
↓
hello, world
なおこれが由緒正しいこんにちは世界。
大文字だったりびっくりマークがついてたりはしない。
計算までできてしまう。
print('hello, ' + 'world')
↓
hello, world