TT Lab
はじめる
学ぶ 学習パス コース

コンパイラ — 小さな言語を最初から最後まで作る

環境とクロージャで木を実行する

TT Labで続きを見る

目標

ASTをたどりながらそのまま実行するインタプリターを作ります。64ビット整数・0方向への除算・boolとintの区別を守り、環境の連鎖とクロージャで名前を探し、実行エラーはその演算子の位置で知らせます。このインタプリターが、あとのモジュール(VM・ネイティブコード)の基準になります。

なぜ重要なのか

意味を定義する実装が間違っていると、あとのすべての実装が、間違った基準に合わせられます。Pythonで書くインタプリターには、Pythonの意味、すなわち際限なく大きくなる整数、下へ切り下げる//、True == 1が、こっそり混ざり込みます。このラボは、その漏れを1つずつ塞ぎます。

ルール

값          int(64비트, 넘치면 감긴다) · bool · 함수 값 Function(선언, 선언될 때의 환경)
            show: int 는 10진수, bool 은 true/false, 함수는 <fn 이름>
            type_name 은 type() 으로 가른다 — 파이썬에서 True 는 int 이기도 하다
나눗셈      div: 0 쪽으로 자른다(-7/2 = -3) · mod: a - div(a,b)*b(부호는 나눠지는 수)
            INT_MIN / -1 = INT_MIN, INT_MIN % -1 = 0 (감긴다) · power(a, b): b ≥ 0, 2^64 로 감긴다
환경        Env(parent). define(이 환경에), get·find(사슬을 올라가며), assign(이름이 사는 환경의 칸)
            블록마다, 호출마다 새 Env. 호출 환경의 부모 = 함수 값이 붙잡은 환경(선언될 때)
            매개변수와 몸체의 맨 바깥 선언은 한 환경 · return 없이 끝나면 0
단락 평가   && 는 왼쪽이 false 면, || 는 왼쪽이 true 면 오른쪽을 계산하지 않는다(결과는 왼쪽).
            아니면 결과는 오른쪽 값(오른쪽의 타입은 쓰는 쪽이 검사한다). 왼쪽은 bool 이어야 한다
호출        callee → 인자(왼쪽부터) 계산 → 부를 수 있나 → 인자 수 → 깊이(동시에 200개까지)
오류 글자   RuntimeErr(노드, 메시지), str() 은 "줄:칸: runtime error: 메시지"(노드의 위치)
  division by zero · modulo by zero · negative exponent · stack overflow
  operator '+' expects int, got bool · operator '==' compares int with bool
  condition must be bool, got int(if·while 키워드) · cannot call int · function 'f' takes 2 arguments, got 1

ステップ

  1. /root/mini/interp.pyのRuntimeErr・Function・ReturnSignalと、wrap・div・mod・power・type_name・showを埋めます。
  2. Envのdefine・find(なければKeyError)・get・assignを埋めます。
  3. Interpreterのneed・evaluate(リテラル・名前・代入・単項・論理・二項)・binaryを埋めます。呼び出しはcallに渡します(ステップ6)。
  4. execute(let・print・式文・ブロック)とrun_blockを埋めます。if・whileはcontrol(ステップ5)、fn・returnはfunction_stmt(ステップ6)に渡します。
  5. conditionとcontrolを埋めます。
  6. function_stmtとcallを埋めます。宣言されたときの環境、引数の数、深さ、returnを扱います。
  7. モジュールのrun_program(program)とrun_source(src)を埋めます。前者の戻り値は(출력 줄 목록, 오류 글자 또는 None)で(プレースホルダーは出力行の一覧、エラー文字列またはNoneです)、エラーが起きても、それまでの出力は残します。後者は、構文解析や意味解析でエラーなら([], 첫 오류)を返します(プレースホルダーは最初のエラーです)。
  8. 採点ツールが、ランダムなプログラム150個(オーバーフロー・実行エラー・クロージャが混ざっています)を基準と突き合わせます。

参考

64ビット整数とCの除算

折り返し: (x - INT_MIN) % 2 ** 64 + INT_MIN。除算: absどうしで//したあと、2つの数の符号が違えば負の数に。剰余: a - div(a, b) * b。累乗: pow(a, b, 2 ** 64)を折り返せば、大きな指数もすぐ終わります。type_nameは、type(v) is boolを先に見ます。

環境の連鎖

findは自分からparentをたどって上がり、名前がvaluesにある環境を返し、最後までなければKeyErrorです。getとassignは、findが返した環境の欄を読み書きします。assignがself.valuesに書くと、クロージャが別の欄を見ることになります。

式を計算する

ノードの名前(type(n).name)で振り分けます。Logicalは、左がboolかを見たあと、&&でfalse、||でtrueなら、その値をすぐ返して右を計算しません。==は、2つの値のtype_nameが違えば実行エラーで、算術は、両側がintかを先に見ます。

文とブロック

Letは今の環境にdefine、Printはshowした文字列をoutputに追加し、BlockはEnv(env)を新しく作って、その中でrun_blockします。ブロックが終わったら新しい環境をそのまま捨てればよいので、内側の名前が外に漏れません。

ifとwhile

conditionは条件を計算して、boolでなければ、キーワードの位置で実行エラーです。whileは条件を毎回計算し直します。elseがIfノードなら、executeがもう一度controlに渡して連鎖をたどります。

関数、クロージャ、return

FnはFunction(ノード, 今の環境)をdefineします。呼び出しは、calleeと引数を先に計算して検査したあと、Env(callee.closure)に仮引数をdefineして、本体をrun_blockします。ReturnSignalを捕まえて値を返し、深さはfinallyで戻します。

構文解析から実行まで

run_programは、Interpreterを作ってグローバルEnvでrun_blockし、RuntimeErrを捕まえて(それまでのoutput, エラー文字列)を返します。run_sourceは、parse_programとcheckの最初のエラーがあれば、実行しません。

ランダムなプログラムで突き合わせる

新しいコードはありません。ランダムなプログラムには、64ビットを超える乗算、負の数の除算、負の指数、シャドーイング、カウンターのクロージャが混ざっています。落ちたら、メッセージが示す最初に違う行の式を、ステップ1の規則と見比べてみてください。