Pythonで作る自作インタプリタ入門シリーズ
Part1【字句解析】 | Part2【構文解析】 | Part3【AST構築】 | Part4【評価器】 | Part5【スコープ・完結】
はじめに
シリーズ最終回!
今回は変数スコープを深掘りする。
- シャドーイング
- クロージャ
- スコープチェーン
これらがなぜ動くのかを実際のコードで検証していく。
スコープとは
**スコープ(Scope)**は、変数が有効な範囲のこと。
let x = 10; # グローバルスコープ
if true {
let x = 20; # ローカルスコープ(シャドーイング)
print(x); # 20
}
print(x); # 10
同じ名前の変数でも、スコープが違えば別物。
Environmentクラスの復習
前回作った Environment クラス:
class Environment:
def __init__(self, parent: Optional['Environment'] = None):
self.variables: Dict[str, Any] = {}
self.parent = parent
def define(self, name: str, value: Any):
"""変数を定義"""
self.variables[name] = value
def get(self, name: str) -> Any:
"""変数を取得"""
if name in self.variables:
return self.variables[name]
if self.parent:
return self.parent.get(name)
raise NameError(f"Undefined variable: {name}")
def set(self, name: str, value: Any):
"""変数に代入"""
if name in self.variables:
self.variables[name] = value
return
if self.parent:
self.parent.set(name, value)
return
raise NameError(f"Undefined variable: {name}")
ポイント:
- 親環境へのポインタを持つ
- 変数が見つからなければ親を辿る
- これがスコープチェーン
テスト1: 基本的なスコープ
let x = 10;
if true {
let x = 20;
print(x);
}
print(x);
実行結果:
20
10
なぜこうなる?
グローバル環境 { x: 10 }
│
└── if ブロック環境 { x: 20 } ← 親はグローバル
- ifブロック内の
print(x)→ ローカルのx = 20を見つける - ifブロック外の
print(x)→ グローバルのx = 10を見つける
let は現在のスコープに新しい変数を作るから、シャドーイングが起きる。
テスト2: クロージャ
let x = 10;
fn addX(n) {
return n + x;
}
print(addX(5));
x = 20;
print(addX(5));
実行結果:
15
25
クロージャが動いてる!
なぜ?
グローバル環境 { x: 10, addX: <Function> }
│
└── addX呼び出し環境 { n: 5 }
└── 親 → グローバル環境(クロージャ)
addX は定義時のグローバル環境をクロージャとして保持している。
だから関数内から x にアクセスできる。
そして x = 20 でグローバルの x を変更すると、次の呼び出しでは 25 になる。
テスト3: 再帰とフィボナッチ
fn fib(n) {
if n <= 1 {
return n;
}
return fib(n - 1) + fib(n - 2);
}
print(fib(0));
print(fib(1));
print(fib(2));
print(fib(3));
print(fib(4));
print(fib(5));
print(fib(10));
実行結果:
0
1
1
2
3
5
55
再帰が完璧に動いてる!
再帰が動く仕組み:
グローバル { fib: <Function> }
│
├── fib(5)の環境 { n: 5 }
│ └── fib(4)の環境 { n: 4 }
│ └── fib(3)の環境 { n: 3 }
│ └── ...
│
└── 各環境の親 → グローバル(fibを含む)
各呼び出しで新しい環境が作られるから、n の値が混ざらない。
そして全ての環境の親(クロージャ)がグローバルを指しているから、fib 自身を呼べる。
テスト4: ネストした関数呼び出し
fn double(x) {
return x * 2;
}
fn quadruple(x) {
return double(double(x));
}
print(quadruple(5));
実行結果:
20
quadruple(5) の実行過程:
-
quadruple(5)呼び出し -
double(5)呼び出し →10を返す -
double(10)呼び出し →20を返す -
20を返す
ネストした関数呼び出しも問題なく動く。
テスト5: シャドーイング地獄
let x = 1;
print(x);
if true {
let x = 2;
print(x);
if true {
let x = 3;
print(x);
}
print(x);
}
print(x);
実行結果:
1
2
3
2
1
スコープのネスト:
グローバル { x: 1 }
│
└── ifブロック1 { x: 2 }
│
└── ifブロック2 { x: 3 }
各ブロックを抜けると、そのスコープの変数は消える(環境が捨てられる)。
だから x の値が 1 → 2 → 3 → 2 → 1 と変化する。
発狂しかけたポイント
1. let と代入の違い
最初、これでハマった:
let x = 10;
if true {
x = 20; # これは代入(グローバルのxを変更)
}
print(x); # 20
let x = 10;
if true {
let x = 20; # これは新しい変数の定義(シャドーイング)
}
print(x); # 10
let = 新規定義、= = 既存変数への代入。
これを混同すると地獄。
2. クロージャのタイミング
# 間違った実装
func_env = Environment(self.global_env) # グローバルを親に
# 正しい実装
func_env = Environment(func.closure) # 定義時の環境を親に
クロージャは定義時の環境を保持する。呼び出し時じゃない。
3. 代入のスコープ探索
代入文は、変数が定義されているスコープまで辿る必要がある:
def set(self, name: str, value: Any):
if name in self.variables:
self.variables[name] = value
return
if self.parent:
self.parent.set(name, value) # 親を辿る!
return
raise NameError(f"Undefined variable: {name}")
これを忘れると、グローバル変数への代入が動かない。
完成!
これで自作インタプリタが完成!
動くもの:
- 変数宣言と代入
- 算術演算、比較演算、論理演算
- if文、while文
- 関数定義と呼び出し
- 再帰関数
- クロージャ
- シャドーイング
動かないもの:
- 配列、オブジェクト
- 第一級関数(関数を値として扱う)
- 文字列操作
でも、FizzBuzzは書ける!
fn fizzbuzz(n) {
let i = 1;
while i <= n {
if i % 15 == 0 {
print("FizzBuzz");
} else {
if i % 3 == 0 {
print("Fizz");
} else {
if i % 5 == 0 {
print("Buzz");
} else {
print(i);
}
}
}
i = i + 1;
}
}
fizzbuzz(20);
シリーズまとめ
| Part | 内容 | 学んだこと |
|---|---|---|
| Part1 | 字句解析 | トークン、正規表現なしでLexer書ける |
| Part2 | 構文解析 | 再帰下降パーサー、演算子の優先順位 |
| Part3 | AST構築 | dataclass神、Union型 |
| Part4 | 評価器 | return文は例外で実装、クロージャ |
| Part5 | スコープ | スコープチェーン、シャドーイング |
全部合わせても500行くらい。
Pythonだと本当に楽。C++で書いたら3倍はかかる。
次のステップ
もっと発展させるなら:
-
配列サポート:
let arr = [1, 2, 3]; -
オブジェクト:
let obj = { name: "foo" }; - 第一級関数: 関数を変数に代入、引数に渡す
- エラー処理: try-catch
- 型システム: 静的型付け
- バイトコードコンパイル: VM方式に
でも、まずは動くものを作るのが大事。
言語処理系、思ったより簡単でしょ?
おわりに
「Pythonでインタプリタを作る」というプロジェクト、完走できた!
学べたこと:
- 言語処理系の構造(Lexer → Parser → Interpreter)
- 再帰下降パーサーの書き方
- スコープとクロージャの実装
-
dataclassの便利さ
自分で言語を作ると、プログラミング言語への理解が深まる。
ぜひ皆さんも作ってみてください!