Open27

トイ言語実験日記4(テーマ未定)

kb84tkhrkb84tkhr

大きなネタはいったん尽きてる感じでどんなテーマで続けるか決まっていない
決まったら書き換える

kb84tkhrkb84tkhr

ひさびさにmainブランチに戻ってクラスで書き換えてみた

https://github.com/koba925/toig/commit/4bdcb27aabe7a5c2ee3a2fc2b025054a6d849aac

Evaluatorにインスタンス変数がないとただselfをあちこちに書くだけになってしまうのでself._envに環境を持たせてみた
メソッド呼び出しが少し短くはなるけど、_applyself._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,
        }
        ...
kb84tkhrkb84tkhr

やっぱり環境をオブジェクトに持つのは気持ち悪いので引数で渡すことにした。
Evaluatorクラスはメソッドを隠すくらいしか意味がなくなったけど。

https://github.com/koba925/toig/commit/b6546a209ba0f74d05dd7ab1a36cd4011e30ffc1

毎回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))

https://github.com/koba925/toig/commit/b6546a209ba0f74d05dd7ab1a36cd4011e30ffc1

kb84tkhrkb84tkhr

さらに、原理を見せるという意味では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)

ほかにも少々修正。

https://github.com/koba925/toig/commit/89101721b56aaff2cd7176fe988c0c8455ba4e02

そもそもextendEnvironmentのメソッドにしていたのがおかしかったかもしれない。Evaluatorのメソッドにするべきだったかな。

kb84tkhrkb84tkhr

この評価器を使うのに必要な分だけの字句解析・構文解析を付けた。

いままでどおりのことをしているだけだけれどもメソッドを並べる順番を変えてみた。
パブリックなメソッドを上に置くのは今までどおりだけれどもその他のメソッドは呼ぶ方を上に、呼ばれる方を下にした。
関数だけで書いてた時は呼ばれる方を上に書かざるを得ないのでそういう順番になってたんだけれどもクラスに書き換えたのでそのへんは制限がなくなったし。

https://github.com/koba925/toig/commit/6b9c20b4a3de7e1830f80671c9f6cfe139b3d281

kb84tkhrkb84tkhr

エラーメッセージに行番号を出せるようにしよう。
短い式をひとつずつ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]のように配列で持たせた方がマクロとの相性がいいかもしれない。
こまってから考える。

kb84tkhrkb84tkhr

とりあえず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が返るものと判定されてしまって警告がうるさい。

kb84tkhrkb84tkhr

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
kb84tkhrkb84tkhr

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
kb84tkhrkb84tkhr

if文ではelififの入れ子で表現しているので.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も同じ。
考え方をそろえてないとわけわからなくなりそう。

kb84tkhrkb84tkhr

Environmentはいじらなくていいかなあ・・・
たとえばここで、nameで文字列じゃなくてトークンを受け取って.valで探す、みたいなこともできると思うけど。

    def define(self, name, val):
        self._vals[name] = val
        return val

もっと言えばトークンそのものをキーにして覚えておくこともできなくはないと思うけど。

いじらずに進む。
Environmentがトークンの構造を知らなくて済めばその方が幸せだと思うので。
困ったら考える(そればっかり

kb84tkhrkb84tkhr

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にした。
ここを通るのはバグを作りこんだってことなのでまあいいか。
たとえば definename.valstrじゃなかった場合をチェックする、みたいなのだったらちゃんと出せる。はず。

テストも全部通った。

https://github.com/koba925/toig/commit/e60c0f113c18d0b7b2d4ecd3847eb6f496990e1a

kb84tkhrkb84tkhr

これ、マクロでやったらどうなるんだろう
単純に同じようにやっていけば動くのは動くと思うんだけれども
マクロで出力したASTの中でエラーがあったらどうなるんだろう?

マクロの引数で受け取った、もとのソース内にあるコードならそのままでいいか
マクロ内でquoteしたものはマクロ定義の方の行でレポートされるけどそれもそれでわるくはないか

やればいいだけ?

いや、そもそもマクロでTokenオブジェクト作らないといけない?できる?
qqqに手を入れたら何とかなる?

kb84tkhrkb84tkhr

わからないのでやってみよう!

問題はどのソースから始めるかだな
小さくやりたいのでCPS版とかステートマシン版とかはおいとくとして
初期にマクロまで実装してたやつに行番号をつけるか
今いじってるコードにマクロをつけるか

このまま続けるかな
気分で

配列関連とq()macro()くらい実装すれば原理は試せるだろう

kb84tkhrkb84tkhr

ちょっとその前にこれを決着つけたくなった

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)

わざわざトークン作って渡すとかないよねー

https://github.com/koba925/toig/commit/24944797fdefaf67cc74bc9b47f37649368321ee

kb84tkhrkb84tkhr

別の方法を考える
失敗したときに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的にはこれかなあ

https://github.com/koba925/toig/commit/acb34e08999860d1e813a23e3201b6c20f07bb71

kb84tkhrkb84tkhr

あるいは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)
            ...

思ってたほどすっきりしなかった。
例外利用のパターンで進めよう。

https://github.com/koba925/toig/commit/ba51fcce17cd5d2f92cd0f3a8454c3588eb2b003

kb84tkhrkb84tkhr

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()を定義しなおす方法もあるか?
まあちょっとおいておこう
少なくともデバッグ中はこっちのほうが便利だろう

kb84tkhrkb84tkhr

マクロ処理のためのしくみを入れていく。
最低限なにを書けば動くかな・・・

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もほしいな

kb84tkhrkb84tkhr

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してくれてはいる

kb84tkhrkb84tkhr

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))

これならエラーにならなかった

kb84tkhrkb84tkhr

expand作ってみたけどエラーの出し方がやっぱり困るな・・・
あとで考えることにする

どうしようか
困る場合はふたつありそう

  • トークンを受け取ってない
  • トークンを含む情報はもらっているが形が決まってなくてどこにあるかわからない

受け取ってないのはどうしようもないので引数かプロパティかで渡すしかない
引数で渡してみよう

形が決まってない場合は適当に探す?

kb84tkhrkb84tkhr
  • トークンを含む情報はもらっているが形が決まってなくてどこにあるかわからない

こっちからやる

式も受け取れるようにして、一番左にあるトークンを探すようにした
全部探そうとはしてなくてわりとすぐあきらめて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

と出してくれる
わかりやすいかはともかくとしてその行だということはわかる

kb84tkhrkb84tkhr
  • トークンを受け取ってない

ところを探してみたけどそういうところで出してるエラーがなかった!
ので新たに作る

このへんでどうだ
_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
kb84tkhrkb84tkhr

わかりやすいエラーメッセージを!ってやってるといくらでも沼がありそうなのでこれくらいにしておく
実用に耐える言語を作るときはこの沼と正面から立ち向かうんだろうねえ

原理上はトークン持ち回りで情報は出せそう
逆に末端でないとわからない情報は例外に入れて持ち上げる
例外に入れて持ち上げる方法はCPSでは通用しないかもっていう心配は少しある

https://github.com/koba925/toig/commit/0d2b4fa3eef089630f5d1d5f96979547530d9d5b

kb84tkhrkb84tkhr

文字列型を作るよ

文字列型を作るときの問題は、今単に文字列をトークンとして扱うとそれは文字列ではなくて名前と解釈されてしまうこと
NoneTrueFalse5などは値をそのまま使っているので(今は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。

https://github.com/koba925/toig/commit/fd895bb999c9c4acda895a3f92a88a8e71f7e689

kb84tkhrkb84tkhr

さてでは。

といってもこれだけ。
文字列をシングルクォートで囲むようにしたのは、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

https://github.com/koba925/toig/commit/b0cc0694fe89f33e7b0197a243b190f670f923e3